From 51dc7342335fcd23aab374177de3cc6ff33c514a Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sat, 30 Nov 2013 18:42:09 -0200 Subject: [PATCH 001/279] added google example --- example/google.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 example/google.py diff --git a/example/google.py b/example/google.py new file mode 100644 index 00000000..bfef2c5f --- /dev/null +++ b/example/google.py @@ -0,0 +1,65 @@ +from flask import Flask, redirect, url_for, session, request, jsonify +from flask_oauthlib.client import OAuth + + +app = Flask(__name__) +app.config['GOOGLE_ID'] = "cloud.google.com/console and get your ID" +app.config['GOOGLE_SECRET'] = "cloud.google.com/console and get the secret" +app.debug = True +app.secret_key = 'development' +oauth = OAuth(app) + +google = oauth.remote_app( + 'google', + consumer_key=app.config.get('GOOGLE_ID'), + consumer_secret=app.config.get('GOOGLE_SECRET'), + request_token_params={ + 'scope': 'https://www.googleapis.com/auth/userinfo.email' + }, + base_url='https://www.googleapis.com/oauth2/v1/', + request_token_url=None, + access_token_method='POST', + access_token_url='https://accounts.google.com/o/oauth2/token', + authorize_url='https://accounts.google.com/o/oauth2/auth', +) + + +@app.route('/') +def index(): + if 'google_token' in session: + me = google.get('userinfo') + return jsonify({"data": me.data}) + return redirect(url_for('login')) + + +@app.route('/login') +def login(): + return google.authorize(callback=url_for('authorized', _external=True)) + + +@app.route('/logout') +def logout(): + session.pop('google_token', None) + return redirect(url_for('index')) + + +@app.route('/login/authorized') +@google.authorized_handler +def authorized(resp): + if resp is None: + return 'Access denied: reason=%s error=%s' % ( + request.args['error_reason'], + request.args['error_description'] + ) + session['google_token'] = (resp['access_token'], '') + me = google.get('userinfo') + return jsonify({"data": me.data}) + + +@google.tokengetter +def get_google_oauth_token(): + return session.get('google_token') + + +if __name__ == '__main__': + app.run() From 6dcaaa5e7f32690ff258a33ed0c69be13d6b3f4a Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 5 Dec 2013 16:33:16 +0800 Subject: [PATCH 002/279] Add note for scopes in documentation. related: https://github.com/lepture/flask-oauthlib/issues/58 --- docs/oauth2.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/oauth2.rst b/docs/oauth2.rst index d9808eb5..43c9cf8b 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -49,6 +49,13 @@ But it could be better, if you implemented: - allowed_response_types: A list of response types - validate_scopes: A function to validate scopes +.. note:: + + The value of the scope parameter is expressed as a list of space- + delimited, case-sensitive strings. + + via: http://tools.ietf.org/html/rfc6749#section-3.3 + An example of the data model in SQLAlchemy (SQLAlchemy is not required):: class Client(db.Model): From e9dbee9976d39e0358c69fa3a28f9f68694eca68 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 5 Dec 2013 16:36:03 +0800 Subject: [PATCH 003/279] Add credit for Brunon Roncha on google example. --- example/google.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/example/google.py b/example/google.py index bfef2c5f..ec43ae4c 100644 --- a/example/google.py +++ b/example/google.py @@ -1,9 +1,17 @@ +""" + google example + ~~~~~~~~~~~~~~ + + This example is contributed by Bruno Rocha + + GitHub: https://github.com/rochacbruno +""" from flask import Flask, redirect, url_for, session, request, jsonify from flask_oauthlib.client import OAuth app = Flask(__name__) -app.config['GOOGLE_ID'] = "cloud.google.com/console and get your ID" +app.config['GOOGLE_ID'] = "cloud.google.com/console and get your ID" app.config['GOOGLE_SECRET'] = "cloud.google.com/console and get the secret" app.debug = True app.secret_key = 'development' From 70ea3ef1af83f24ffc70f0f9adaa5dc91d589eca Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 5 Dec 2013 16:42:07 +0800 Subject: [PATCH 004/279] Use String instead of Unicode, use Text instead of UnicodeText. Because Flask-SQLAlchemy has convert them to unicode by default. Fix another error in documentation. related issue: https://github.com/lepture/flask-oauthlib/issues/56 --- docs/oauth1.rst | 40 ++++++++++++++++++++-------------------- docs/oauth2.rst | 33 +++++++++++++++++---------------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/docs/oauth1.rst b/docs/oauth1.rst index df56bfef..0f865162 100644 --- a/docs/oauth1.rst +++ b/docs/oauth1.rst @@ -51,22 +51,22 @@ An example of the data model in SQLAlchemy (SQLAlchemy is not required):: class Client(db.Model): # human readable name, not required - name = db.Column(db.Unicode(40)) + name = db.Column(db.String(40)) # human readable description, not required - description = db.Column(db.Unicode(400)) + description = db.Column(db.String(400)) # creator of the client, not required user_id = db.Column(db.ForeignKey('user.id')) # required if you need to support client credential user = relationship('User') - client_key = db.Column(db.Unicode(40), primary_key=True) - client_secret = db.Column(db.Unicode(55), unique=True, index=True, + client_key = db.Column(db.String(40), primary_key=True) + client_secret = db.Column(db.String(55), unique=True, index=True, nullable=False) - _realms = db.Column(db.UnicodeText) - _redirect_uris = db.Column(db.UnicodeText) + _realms = db.Column(db.Text) + _redirect_uris = db.Column(db.Text) @property def redirect_uris(self): @@ -115,18 +115,18 @@ And the all in one token example:: user = relationship('User') client_key = db.Column( - db.Unicode(40), db.ForeignKey('client.client_key'), + db.String(40), db.ForeignKey('client.client_key'), nullable=False, ) client = relationship('Client') - token = db.Column(db.Unicode(255), index=True, unique=True) - secret = db.Column(db.Unicode(255), nullable=False) + token = db.Column(db.String(255), index=True, unique=True) + secret = db.Column(db.String(255), nullable=False) - verifier = db.Column(db.Unicode(255)) + verifier = db.Column(db.String(255)) - redirect_uri = db.Column(db.UnicodeText) - _realms = db.Column(db.UnicodeText) + redirect_uri = db.Column(db.Text) + _realms = db.Column(db.Text) @property def realms(self): @@ -157,14 +157,14 @@ Here is an example in SQLAlchemy:: id = db.Column(db.Integer, primary_key=True) timestamp = db.Column(db.Integer) - nonce = db.Column(db.Unicode(40)) + nonce = db.Column(db.String(40)) client_key = db.Column( - db.Unicode(40), db.ForeignKey('client.client_key'), + db.String(40), db.ForeignKey('client.client_key'), nullable=False, ) client = relationship('Client') - request_token = db.Column(db.Unicode(50)) - access_token = db.Column(db.Unicode(50)) + request_token = db.Column(db.String(50)) + access_token = db.Column(db.String(50)) Access Token @@ -186,7 +186,7 @@ The implementation in SQLAlchemy:: class AccessToken(db.Model): id = db.Column(db.Integer, primary_key=True) client_key = db.Column( - db.Unicode(40), db.ForeignKey('client.client_key'), + db.String(40), db.ForeignKey('client.client_key'), nullable=False, ) client = relationship('Client') @@ -196,10 +196,10 @@ The implementation in SQLAlchemy:: ) user = relationship('User') - token = db.Column(db.Unicode(255)) - secret = db.Column(db.Unicode(255)) + token = db.Column(db.String(255)) + secret = db.Column(db.String(255)) - _realms = db.Column(db.UnicodeText) + _realms = db.Column(db.Text) @property def realms(self): diff --git a/docs/oauth2.rst b/docs/oauth2.rst index 43c9cf8b..e681f898 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -60,25 +60,25 @@ An example of the data model in SQLAlchemy (SQLAlchemy is not required):: class Client(db.Model): # human readable name, not required - name = db.Column(db.Unicode(40)) + name = db.Column(db.String(40)) # human readable description, not required - description = db.Column(db.Unicode(400)) + description = db.Column(db.String(400)) # creator of the client, not required user_id = db.Column(db.ForeignKey('user.id')) # required if you need to support client credential user = relationship('User') - client_id = db.Column(db.Unicode(40), primary_key=True) - client_secret = db.Column(db.Unicode(55), unique=True, index=True, + client_id = db.Column(db.String(40), primary_key=True) + client_secret = db.Column(db.String(55), unique=True, index=True, nullable=False) # public or confidential is_confidential = db.Column(db.Boolean) - _redirect_uris = db.Column(db.UnicodeText) - _default_scopes = db.Column(db.UnicodeText) + _redirect_uris = db.Column(db.Text) + _default_scopes = db.Column(db.Text) @property def client_type(self): @@ -131,17 +131,17 @@ Also in SQLAlchemy model (would be better if it is in a cache):: user = relationship('User') client_id = db.Column( - db.Unicode(40), db.ForeignKey('client.client_id'), + db.String(40), db.ForeignKey('client.client_id'), nullable=False, ) client = relationship('Client') - code = db.Column(db.Unicode(255), index=True, nullable=False) + code = db.Column(db.String(255), index=True, nullable=False) - redirect_uri = db.Column(db.Unicode(255)) + redirect_uri = db.Column(db.String(255)) expires = db.Column(db.DateTime) - _scopes = db.Column(db.UnicodeText) + _scopes = db.Column(db.Text) def delete(self): db.session.delete(self) @@ -175,7 +175,7 @@ An example of the data model in SQLAlchemy:: class Token(db.Model): id = db.Column(db.Integer, primary_key=True) client_id = db.Column( - db.Unicode(40), db.ForeignKey('client.client_id'), + db.String(40), db.ForeignKey('client.client_id'), nullable=False, ) client = relationship('Client') @@ -186,12 +186,12 @@ An example of the data model in SQLAlchemy:: user = relationship('User') # currently only bearer is supported - token_type = db.Column(db.Unicode(40)) + token_type = db.Column(db.String(40)) - access_token = db.Column(db.Unicode(255), unique=True) - refresh_token = db.Column(db.Unicode(255), unique=True) + access_token = db.Column(db.String(255), unique=True) + refresh_token = db.Column(db.String(255), unique=True) expires = db.Column(db.DateTime) - _scopes = db.Column(db.UnicodeText) + _scopes = db.Column(db.Text) @property def scopes(self): @@ -301,7 +301,8 @@ and accessing resource flow. Implemented with decorators:: toks = Token.query.filter_by(client_id=request.client.client_id, user_id=request.user.id) # make sure that every client has only one token connected to a user - db.session.delete(toks) + for t in toks: + db.session.delete(t) expires_in = token.pop('expires_in') expires = datetime.utcnow() + timedelta(seconds=expires_in) From d2a496c59704b07a158496c5778fe3b36b32f02c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 9 Dec 2013 13:28:25 -0800 Subject: [PATCH 005/279] Add support for OAuth2 state parameter --- flask_oauthlib/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index bba83bdf..fa7cb0d6 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -437,10 +437,13 @@ def request(self, url, data=None, headers=None, format='urlencoded', ) return OAuthResponse(resp, content, self.content_type) - def authorize(self, callback=None): + def authorize(self, callback=None, state=None): """ Returns a redirect response to the remote authorization URL with the signed callback given. + + :param state: an optional value to embed in the OAuth request. Use this + if you want to pass around application state (e.g. CSRF tokens). """ if self.request_token_url: token = self.generate_request_token(callback)[0] @@ -467,6 +470,7 @@ def authorize(self, callback=None): self.expand_url(http://github.com/self.authorize_url), redirect_uri=callback, scope=scope, + state=state, **params ) return redirect(url) From 3db6d8a7293af2d0227103b9b6a21573d9fbe496 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 10 Dec 2013 10:13:47 +0800 Subject: [PATCH 006/279] A new way for generate state. https://github.com/lepture/flask-oauthlib/pull/63 --- flask_oauthlib/client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index bba83bdf..8a9b3110 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -462,11 +462,19 @@ def authorize(self, callback=None): # oauthlib need unicode scope = _encode(scope, self.encoding) + if 'state' in params: + state = params.pop('state') + if callable(state): + state = state() + else: + state = None + session['%s_oauthredir' % self.name] = callback url = client.prepare_request_uri( self.expand_url(http://github.com/self.authorize_url), redirect_uri=callback, scope=scope, + state=state, **params ) return redirect(url) From e86d62bd861c4cbd81e182ed0d9bbb4da066d941 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 10 Dec 2013 10:17:07 +0800 Subject: [PATCH 007/279] Documentation for state in request params --- docs/client.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/client.rst b/docs/client.rst index f2f67ecd..3e64447c 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -28,6 +28,19 @@ Find the OAuth2 client example at `github.py`_. .. _`github.py`: https://github.com/lepture/flask-oauthlib/blob/master/example/github.py +.. versionadded:: 0.4.2 + +Request state parameters in authorization can be a function:: + + from werkzeug import security + + remote = oauth.remote_app( + request_token_params={ + 'state': lambda: security.gen_salt(10) + } + ) + + .. _lazy-configuration: Lazy Configuration From 21f5c3e73a023d399890d91d84f69c4ac0ea284b Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 10 Dec 2013 10:47:49 +0800 Subject: [PATCH 008/279] Add param state in authorize method. https://github.com/lepture/flask-oauthlib/pull/63 --- flask_oauthlib/client.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 69593b96..3aea1c51 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -442,8 +442,10 @@ def authorize(self, callback=None, state=None): Returns a redirect response to the remote authorization URL with the signed callback given. - :param state: an optional value to embed in the OAuth request. Use this - if you want to pass around application state (e.g. CSRF tokens). + :param callback: a redirect url for the callback + :param state: an optional value to embed in the OAuth request. + Use this if you want to pass around application + state (e.g. CSRF tokens). """ if self.request_token_url: token = self.generate_request_token(callback)[0] @@ -466,11 +468,15 @@ def authorize(self, callback=None, state=None): scope = _encode(scope, self.encoding) if 'state' in params: - state = params.pop('state') - if callable(state): - state = state() - else: - state = None + if not state: + state = params.pop('state') + else: + # remove state in params + params.pop('state') + + if callable(state): + # state can be function for generate a random string + state = state() session['%s_oauthredir' % self.name] = callback url = client.prepare_request_uri( From 2b4fe31ac5a94142242fffd00a57a2d3c8965d14 Mon Sep 17 00:00:00 2001 From: mdxs Date: Wed, 11 Dec 2013 22:56:28 +0100 Subject: [PATCH 009/279] Fixing typo in index.rst Using "designed" instead of "desinged" (typo). Using "same as" (comparing things); instead of "same with". --- docs/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 930b075c..ea88aedc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ Flask-OAuthlib ============== -Flask-OAuthlib is desinged as a replacement for Flask-OAuth. It depends +Flask-OAuthlib is designed as a replacement for Flask-OAuth. It depends on the oauthlib module. The client part of Flask-OAuthlib shares the same API as Flask-OAuth, @@ -17,7 +17,7 @@ Features -------- - Support for OAuth 1.0a, 1.0, 1.1, OAuth2 client -- Friendly API (same with Flask-OAuth) +- Friendly API (same as Flask-OAuth) - Direct integration with Flask - Basic support for remote method invocation of RESTful APIs - Support OAuth1 provider with HMAC and RSA signature From c31ff0375b69c25f1d50ce0579a5d06b7f6671e8 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 20 Dec 2013 15:47:11 +0800 Subject: [PATCH 010/279] Add to_bytes methods --- flask_oauthlib/utils.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/flask_oauthlib/utils.py b/flask_oauthlib/utils.py index 3349d064..6a93390c 100644 --- a/flask_oauthlib/utils.py +++ b/flask_oauthlib/utils.py @@ -19,12 +19,17 @@ def extract_params(): return uri, http_method, body, headers -def decode_base64(text): - """Decode base64 string.""" - # make sure it is bytes +def to_bytes(text, encoding='utf-8'): + """Make sure text is bytes type.""" if not isinstance(text, bytes_type): - text = text.encode('utf-8') - return to_unicode(base64.b64decode(text), 'utf-8') + text = text.encode(encoding) + return text + + +def decode_base64(text, encoding='utf-8'): + """Decode base64 string.""" + text = to_bytes(text, encoding) + return to_unicode(base64.b64decode(text), encoding) def create_response(headers, body, status): From 48085c99f1c7ac1012d8ee0eadcf03573fedb476 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 20 Dec 2013 15:58:08 +0800 Subject: [PATCH 011/279] request body with bytes data. related issue: https://github.com/lepture/flask-oauthlib/pull/65 --- flask_oauthlib/client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 3aea1c51..8027d734 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -16,6 +16,7 @@ from flask import request, redirect, json, session, current_app from werkzeug import url_quote, url_decode, url_encode from werkzeug import parse_options_header, cached_property +from .utils import to_bytes try: from urlparse import urljoin import urllib2 as http @@ -433,7 +434,7 @@ def request(self, url, data=None, headers=None, format='urlencoded', uri, headers, body = self.pre_request(uri, headers, body) resp, content = self.http_request( - uri, headers, data=body, method=method + uri, headers, data=to_bytes(body, self.encoding), method=method ) return OAuthResponse(resp, content, self.content_type) @@ -550,7 +551,9 @@ def handle_oauth1_response(self): _encode(self.access_token_method) ) - resp, content = self.http_request(uri, headers, data) + resp, content = self.http_request( + uri, headers, to_bytes(data, self.encoding) + ) data = parse_response(resp, content) if resp.code not in (200, 201): raise OAuthException( @@ -574,7 +577,7 @@ def handle_oauth2_response(self): body = client.prepare_request_body(**remote_args) resp, content = self.http_request( self.expand_url(http://github.com/self.access_token_url), - data=body, + data=to_bytes(body, self.encoding), method=self.access_token_method, ) elif self.access_token_method == 'GET': From 265f36fb7fac426d662fbdebf29e8aad01e257d2 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 20 Dec 2013 16:01:45 +0800 Subject: [PATCH 012/279] Fix to_bytes when text is None --- flask_oauthlib/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flask_oauthlib/utils.py b/flask_oauthlib/utils.py index 6a93390c..1b8f0aee 100644 --- a/flask_oauthlib/utils.py +++ b/flask_oauthlib/utils.py @@ -21,6 +21,8 @@ def extract_params(): def to_bytes(text, encoding='utf-8'): """Make sure text is bytes type.""" + if not text: + return text if not isinstance(text, bytes_type): text = text.encode(encoding) return text From da6883ed7822ca079da4cf7e5e9d51b5ecf08a89 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 24 Dec 2013 16:04:43 +0800 Subject: [PATCH 013/279] Add artwork --- artwork.svg | 67 ++++++++++++++++++++++++++++++++ docs/_static/flask-oauthlib.png | Bin 0 -> 23909 bytes docs/_templates/brand.html | 3 +- docs/conf.py | 2 +- 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 artwork.svg create mode 100644 docs/_static/flask-oauthlib.png diff --git a/artwork.svg b/artwork.svg new file mode 100644 index 00000000..17f8e667 --- /dev/null +++ b/artwork.svg @@ -0,0 +1,67 @@ + + + flask-oauthlib + Created with Sketch (http://www.bohemiancoding.com/sketch) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_static/flask-oauthlib.png b/docs/_static/flask-oauthlib.png new file mode 100644 index 0000000000000000000000000000000000000000..67c7552295cc58a618ddbe7a99015933af248069 GIT binary patch literal 23909 zcmV)TK(W7xP)M003PqNklM_luaNlOwg2_)Q>P6(3_l3UV}rZcyf z-d+ev2_&T9UP3dWg(ih!COk?=07FQ?fDQO5W8)Wo%95<7^;+%j*PXdWBiR~b`~o`s z{&SACvpU~M^POFN-_F=aKJt-|eB>klMMeILcrI6NQ*K_=m(vyXM8$d{Mxg@~gk&HY zax)`S<^57V^rGi{Xyw+I<&N$hk)IpSZ<*0MwRd7)RccgbM5-VY&m=-D#6;OLQMM?R zlg!o^Z_C?}zrMJ>=&im~NGJMqCY$W@Ar$?@hfPya>Gb4;+``^d+D>hq**z{3rS<62 zo~=*6lytUKwoJBMF*&kl*+W8!F-FQ4iHUcP+*0;x$;&<6Ijy$;MZMrdBMLq&Zkrfg zker`Wni{)f%JzzV+U(P=N1r2;D`BJ(ql{3T8n>(Zt#n&#bM(01`w)n3^+A@*nVHv7 z(6X@YnoMoFXnWb#5v|&F>ymaXfZ{=09G&xV-Dq9^s=`+FexRNqznOsv5kA3)K) zK6rX3_0P_nvg7Kux<>VONgiUF67m(wmn)$_q2g=>+45x%32D9h^y<;4)n5B_NoDKS zsaFQWi`1B_RK@dE4@T=+UeW6VCA!%M$=cYd*XEqlJa2tngPj@-FQ#0TN`*$NHr`lO zip4}?67s~dMT1^(VgJ#aEiEId%}#H6TZdk|ZPzSqIC0~$P3WERaRo?Hw(9Gk#ewHn62qf(tI$_`_y0*D-r zw2T((ZPa45mo)2^G*qO}Ddu#KUOV=C@fS6Gw2zZIyX#9^uicdMvaVs@6wAzVfeKSh zRA4ws{d)9CBc)HTKHXAsvn6ED@)d~WO32HWFg#MW*r3TPUe#caw4rj$aGL5B{s!F+RN+*UTxgb2D&-g|WpB1U`gGc9o86jq=#ds=q_bt@ z$R^5`Bd$=9G0IIf-U#`slx7=F&|bF!M9P45zQof1>)kI=xS#pLxHu)pdS< z_kyLPw(2lc!ZfEl$*0vADQ>WSyKS}2KAUZ@Rf|4Jy;9PGcN3GVK*9)POftg+MY0pA z+`-b??DU+~UQsV&ut=>_%~=t@Z`%Kr^gf6l@_uNX(DFZ8uX(X}wH|{-MwsnFbDXJE z+yK3LZ1k!IJFT-t`@mEksYtH4GSkd3#sp`nmdG|#N{6S_d)68qf&p?>JG;O3t+E@# zA9OjvqJMCr<<+Dv+j{fn(ie0XEK+TT%S?Bc0t0mGu-?XH zD>PK6*R8YIDvgG!v%t7tjJ`#^6DqpPiPd*a^S4%=yiBvfA~T)g3e$~~0MKKjU6y;^ zR=adN{)$zY;5_qGnX1rWXTf~#*5W&=wd)*tOYK#&{(;El2`d&M*@|m)(BrcV6+f zqf_n%C4Oj=qf=}uzkCl2Ve&9m9o*zD+C=E+Ql#0ZLpJF=rX4p0qFLUmpx*OHp3gC%i+s+gTIsGhi=v3&|>DB zpirMp-q45PjZ|%sbIex_V6UYf@CQ2?Q0`o(|0?&Z8XQNlGqfGIu8p3!sB`ge=l*WU zbd2&jf96`#BtY5<5BZ73w(E7UmY7St$)5W~S?W z+4%}Y$k^>scYDA_{SH=avd{RkT3fszoR!#{^n!C_Y(?hb#S9eHI)hhK!sL)}~9&d?ox z5(Q|$c+pI!%h+s#V0aa- zF~c%1=|r}$mO4Czu2JM6O{SP_l4dVR z8eXwW)Tz+zO}&Ua+w0ztfrGX#P5K$o=WWaFlQLc$2~$i}V52TTpN$Hq*PM~}XreFS z*cQ9+7!Non_UmtL`G<##w=kf_S6t%)`5A>MvYnwg_}s>|PIQFj$dK`?f+G z^cX7WFwZn4-n7QM%kepjj8bQoDXNqzx7jO#;q=?1&J-nP==Zh`&Gs5jk7}1WQ^m+~ z3Rv!6-0o#bgU$3smy4-TsmWeL^(yoUW9_s?2Lo~i2bH^RBAt9v`%ZRR;*MJ@||p|jJNayw#u1Sb$-rsnY|gu zs@NaN9HU-MjDBR*pFFM$K$UBJ#aZGY<=1}d0gZ;5>+7m4b%*t)Dzeg0scaXjP;8aA z0Mz&bb~(`bKIJh>?9JAq%K{UWT49X?o#j%^S~Oa(NUaJJHG55PupC$UvMQ^r5Vy-V z`BDt|{PImfjnT+aW2&T0dI39hR92rGe<`ykTAa93Cu;`<-RgM%6!ZV&yTGyH8B5$x~O@KCsXnD4wZ+#70x{UyS; zLr3Tf-^)JZfG!WuhEEMF-S5thAHpxgj9{2%hkp+tG>2Qmm{1Vn+4GHIUdRcCR1v-w zUJw1@|JxL93gtt__|D27pYmhJrkqiZMWc&aA6oaxC!`1cW&5VKpur|x=R%DSX0A)&zmcxLUIu~1{Um_bsN_0y}xYC8HL<#=TO(kX_NuZ4dB5#%>vy!p79RBw z&AI-q{!_n^2IRZKbr#A8!L$C+!7Fe|S>HP*dIH zV_N+}${8+kmGf0=v)(%SDlC;TSc6UCrW%0}W(XSX0W`{~8hKLpll%JiIl5wVj&gu8 zMePsNf95eMK-{1EnvaQtlt1`8PwR1@ZY%xXYx{R;8C&f2B_A_NjTXCQ4ApL$8OADe zzH-YgcE1&R8IZPj|9`z|)&fH)fPRb8;JtbWo*_0*ltwagp*4iE9^hYecWY7b$LjH zdzOCTNhv^qE8OH15lC9%`<|0=u#9d=?RKj&-APi)eL=m4<(guhE<@!yQ=K+%dCFps zdRw22qmi-Gayv!pOtQs;?zh!}7Wj%f8$D>2)4bpZ9=FO4ol??zvkj-kM%|_>MuC%b z+o=@<+f+`gtSEjeFO%oUbI(yEy7P4Kj;fb&?8LKg^OA4u9X)7ie_dD?-q99*9_9pxQGU27G=>m1 zgo{HI3|nP(>`#O-!3k@0xFTE`{wC}RAv_i4WgkBnLRc2geowc=HwLDzDg5s+anR^D zH2>)0A35@3i^Ptwc46Yt z{+1(NRi1Z*7w)U=dii}_o>ISm_tsZobWe!)n9+jq=C||u+WinERN;#BVMaong zuf_zEO*PYnE_I&Mj8UOVS+-FMBt-hXyLK;2h*VnRZmr@@H($n5Z#aCtR!Nzz1VyIn zvO)^92_q+cJobnlMcx8HY_!G6( z$v7Nw6{b2#jfuvIbnDP)o7JlPt#j4;cP&ziRjXB|LZwuEop`Y z^8CRo-iv-4MCK|$v8fuYmj(^W#?}_T_H;vN@ZO7!If9Gsik5wK=~thM0^mGfca9v; zY4O0Dtgy{mt8KAcvvyrdWN#&{#(Q49X4|Z{ z)+Pk^i`81S+vDR3onwsWyyU&(4{v) ziu592L8^2Ng7hMwpr9yrQ0X0{7e&N^3JQWCA_^#i%7+R_Q3UC|2NFW+W`7^&ojLdH z?9M{i_5IKJKHRrSX6DU%xBc(ERg7JiPwtUa*=OzzIUbs?`h9y#94sMsj(}(hGa3#~ z%7a+Ahj&N=08&{^4J05GWFg|9pfG*k_bRHTj`{_c5z<1FsH^G zxpM^YBU4Jed8E3>;~vK~ur6u?&5!_*pHY0rQ{=tncPjA^gIUcVbhB>11sB;$6`Mx* z0VxbMkGvU2IM0KKA~)}n%{;RJ_wpNOc$F?BP?MsB`Q@fGVKOJkW)ks80I14rk^ykB zie~z1He}3COJbMEokH&XN5+59%0rcZnG;xpUujDi5+IuLAzg{|yU$8dlU5|qjBiNf z2}IF~Bz~n5Vi3;~+`On)(u5N?QAMwK4lsvk#Nb{Ia)nmpk#m>0*$o#KvvVC);+Twxvt_W`WI|L8~r5->_r%g+%R$%FJZ zyQ~I9`GaIyBQ~_<5(lVj%-(N+mu$bO4tqJmLuOHD0i$fMS0#3kOdn%+EoD6ed4O2N z0ijf3Dx)Zd1cZm!h#P=(rcp#+wC7VMwwWk*hCBTF+xHE>-)9j3=FsDMfvH z(ZJ8*m7o!kNI(eVQ4RqBr+I@ojns|XGA(wR+zIaJ%R3^*zf;%4r885d+jXW=82OJ; zfV%YNKH{wXGZ=uOevjJo#yvHJ5XafWWRS`{$HTlL+ejme56p6f5W~atq8hoKm6d>z zmp8bgOw4}PD-cdQ_v9gW#=yLTuM#q=>yt#Q%<0YF0Dzm-+=Kk5h^8i;X-*uX8rW(s zlfg#H_<1Pb0w<{EI2+Gu;1UUb4#9B%bg=!t4z#Bl5y*dtnk>~A`8PDwSM=qx^J~tR zoI~#5%SB!~T^6y>h)rOPjWj0&`43T%4m?O6n?k*tB-+r0RCZC$!KJ&7G-h#v1Kj7J z6)p#6u#1a4>@yp|9Nf&|7n0~_n<+{&5-5WFhj@VX06->7)b;!zZ+^c#*K)}zB0`?@6x=xauBFA4G z*Jf~XE`38E=Qse6%q$8a{~7M4Gu3#VG}fwwDb63H&`o!BSJL^#=4=0sG+wk6`Q}{X zAMVxJSl%Uz;nt(C#Zk`lfXBkjBAZXGzg3A{T=qCHLwJT%KIJhws4f{i@-v!50APb& zODSGGJSA?5GH6I>zHLz4f71!sBF#72WENfn8qCA42`T;@C{(8hXz@hgCz4PVlWWcL{YP( zJ*fPj352qIg$saG7HDcrLiXetlO<=k&9q`%LROd_Wn=YyIYE~og|}T9XiBuc>rdn6 zdz$knS83%yu1jGTcax9r$wGIds{dy>NNr+S4!YY^L0~tRd6+Qf0&jbnF_rlrDMr@4 z1uWB@7DroJ6NL~wLg>VP0AN48RDpWt%J_uwk`v_UOEb&7d{DWHwB%RiN>2UHf}4fu zLJivk4r2_NoFSPf5C`-mjUU*8ix(YsLMyKDH_L&s4xWyB?B)b3$>MWj98{w}InN5x zSZxynF*M|H^882lJBf4v+`Oj}`+laZ$l12e(d#j@Zwvixg_uMp0I-#&$bW(obf=<& z>UBK?*hdLjEO}+6RFx$X z?^uoG0l6-j^01V4n2nc~CP%x0RC0{sJuGo@P41C8;xrzTe3Eo?w!TmJX^-rdMTa__|OM7a%eVt|i(xf5cc2=9^!fSZ+6M+hFJc#-o=AjV6< z|AtFEL8Lh_7ZdMeP=5ft#Oq|R!DmnAViqu;ZY1%uk8>c5*U4rXJvhNp2l`K6T&$-T ze{;z}y0&8@Pm>D?8s+(dEC9~YnFvb<9_)Iz#=CMm-1ZC8OTMIpph!A%4glE2;|Rf{ zGTkW6IN(Eac^Cydxo%lhVMa|^M?RZ`Sfj4w9%S-8cia4%bAd60A+#Zpzo=_F?xq9F zi8V^hUQVFKfx%m3vf8`~mH3k+`rBrEaE0|WrjK4v;0UEHM*zrXCFNEA4&N|bZim}` zVcde|h=r0Y!woQ-xJArGMQ@ax(fVG3Ozk`$XjHwmV&B(t|f~X30E~G zi>;LLh__`jf^ZM_^Is}K!+0APpXm;+%W*EKNgl#LGWbemTP1drLVKO<#tnX>I6^hL z5&W*p&n!{^IL2V>io+O`I;m-p2Ka=cZ~R?IFS`qS0Dyn=R)awWdQntuxRGQtn`nx& zldE(?YQ z11zJ6?L_#EWXmM)O&Z%Nr@^9DL?S_=CBFdxpX%#Uow4i3%B^vm=_#WVvcmQ4SWJfQ zw}Qbd#PqajZPB~~%;Re=Iv4<9j5e~a0w3Y#B_9?4d2?XaWjZj=#|77#Bz~bf!^q|f zpBQ^nPH>!BJb~LXw>?pXf4M?iT9U*r4|y3zTN)7-wDmfQGyrzfNzaVu&rEGNRc?h_ zPR=Ske1|%gjIhS~6RnUSQH-t>cHn|QmLKUE%F|?Wlnh6^P3XooE|A4Y#|U*B&T$$S zpF76c>adH`WU#_9I8~9~NZ}%DDDD-(XhY@T%>p(8kj*sms$$-??*ON&B)^czR?3=%vd_*Xi=Q~J?q@rmA&bo%;fy22ycfy* z#SRkb;Bayb4B8-Xzz+aMa>wfNJ>!Ydp#cR_=T&(++maO&WE2?yWHOyNBq-D}w%KIc zkye~_T&6cdH17h_yp}ExpODRqL@|O)J|x~tJDUg0CW2?lU_AxQH5W4|U=B+7oH$x? zieG(vG*1DWD8NI8R#eqTeMn{}rKre3uF=+OwmBQWlj9H`u*?dnu)!<*-y|my` z@dv*XgpPSnsdhADH*RacwL^kJbvjX;`M^XX)hsY!QZ=7Pt6isIQnlMcc?LJ%QNUwS zR{$djvy6k^$;&Wyu-D)Z22hz7tzBA=ix^sw;!UzyK@ksHQA68qyTmuvaahR^mHHxz z&Rk|Eov0q<3G^WU0&syR2(v!x&5ZUl#2@@l%+LK|VjhjZJ;!ALvROjGAT5_RMn=Uj ziwwq_%PeQYq`hrw$^^2RKrU-x*4QXOVT{7X*W|a3!3AV{P=G=xPaUqK!|b9^+YZ`G zeZ-(Y>8$aPn)NtoD732Z@CPMqw4&>@(b@AHG0c=66b@3uUQPy(#)s;*ugx2q2)$z` z^Z2(qy43x|x3~Z}O&7v~dbgTTONH`M+>GHK^B#NKZBgbpok0|Nd7o4-yDf}2$Y32Y zH?fc`UbS6SEU#c6ZzCSxc!Wd_a4#VY!sVk$w%~|mlJ_8$ZD<@)L#l ztokl6n~%t3sLx{wVFc;C%RTr7rR9% zv@dg#zo_Yz(zl&MJ-MNoBgQz_wYCHFQshC zQ@5amNE9!U2Dn(PT53^7{@FRKvp6C2FL6Rx(|s}78eXX&QC78*{gNSpk9u<2RtxBE zlPv#(w3bm4<`|l(Xu2%kBhijk@+zADuOV?>MS$~3xY$Q=DJE{OS_-+%=f(ZhV9qO; z z#XL#}6)b&BT~SH_q6@Js!^H%jy`0b3Y%ep0RHpk_eX%SggO`o0TWj-%6k-6r<0-^O z0ZL~%UAV%2AKyo)k!_oKoP%880SC_X6%Npjon9=>T(qK8P`;#h0XWMG9tY^SPsWK8 z96y}Zt0^IBx=sfGuF#*5AjI#JsOLc`K;11_w-lIYv+MF23eZr5DBc2QlFN25El}C^ z78$%xyvGYM7T+<1a6)Vppdv=ro#9t6lf18q0F*;$!ck5u4^xRq?574oW&TxKkpbm+ zgm5HayvkJoX6oG(VfgO(MRG`pU087V_hnJ~rUuwoQxgdoCHaUXMyUF*i>tKscw1)! zlXP*4uz^ga0EIIG_=Nnrs4IYBYVka661)oQU-}H+@h15#DK`RGN-+;w(E;jvEL%Ej zDe6TlQhz8{*h^_uQ?`@BlO9UVyWEWgjQgx-y;=P?ktAfzY@I`w*SsoEW$Lj-e=Y$4 zQ-X*;Mben2tRsV05z4Z|@TlxZ-+VF|Pq>CjJzdGsj0Zk4R}jZMV2sDR9mY7^tRDW!H7)f*JrCuhUR#9S-v=eS$Cx z^6?1(mpq=${mg#Gsp^8mN7MUs_V`t!$h1!0=a7I=hHhp-Hjv6Nws4*9J~x=l1oB5y z#4PYo!o0Mu@nkTKqP#;UZ+aa`k;cQhhGNX1K8g5_FK9p!o+g!5cDqeOt(ECaD*vOH zPvqbj=cz|CE_2u?GoccDOsKV%3CK30HTNO`B7$L@12P##oIbhXKfZIN%l~~k$mwpm$|}W9z!Td&mdeB_p=Rf z^Oe3J(LCk)u+oR(4WAD-M?agWYzjJZ1^^dJf?!Y1!dg)|2uF<@-u-pYz-saRoC}Ai-T~*!x9yd?hj=_&e;RUmp@q7kM@L0MyLjjsi z7($eZ4S0rHBbX0-YRvHbjKq!8=u2LNj)qpGhO$Kg`XA*iHRS?ZZR$l!PIJ)GB%2vp z(StU{U?1gC%|3Htqub=~T z0d&v1$IQ5mlX=p8L(YZkTfRVfwk-qpnN;F2s<6|cQbInyBa7D%3h@niQGjC1(Kvxn z;+PGLwCQTmi~&9{OF0XeMywt-yKo*0)o|`kCOgg2_2e>Zy-t&MTxP$Kb)C7yKh*S+ zXnQ%z1Lj}nIYDEuyWL3=J!lcIf)dReqybkMLZotWhHW1vc6j~g#OFJug{tpt6##IO z+5w;0jo8L1t~zRDMc7Cx<5+>4p+xxDb*o5Y3iC*3l+SD=6OAOCO(vh|UJN6Oqd1Qr zDD4r~PGuu6aLuB(+F*!yzcG|kCQRz<-#Ec(`f>OsnmRDT_Hv2wJQ}c3(2o?r#UkQ# z3%&8r=+IkoPR7rFh!8!$Hv<5_Q88dGX((5KIez<2Z3NuB;OD1T;U=3!PW#=kp8=K< z?O_rW;uy~3yv-xhg>>BP@#7Y5Mk;QO`Z1;(ahh!9KYODBf8t_pz<#FAoB&`IMOA1$ zb7@j@dmtb*N<85zlq6{2ql8sO?w^4B0>?^Iam!)pEgk#>GftZvm}Ec23@=KEgvvFi zppSOqIb*9$J}*EmylcZp-s9ud)`kUxnxO|P`M%3{1ja~Ct*@XDo9}C zjGVNJ=9dy}WS!65_?8VLCViuEZ^n=eK&r*|2n0=7OFPQ*6DbbVw!(%I@fNFbG0Z-v z6reH8^Q09i!kGh1WxS166ist3;BV9n^4jSn-O*F9&I0u*K<2N{eb%(iTAlWh*ka3_)Ym(L@E}S#4MeQr#zy7?-`9V+}cOW z%;H4b>XLk7J(wuQ8d^~dLI@+t#NX&kek%o8DL`~3jh`v)p_p9dkg6n)aMc!St<4F} z@vwgPh|MG~Wwhsx`m+HP?a>4mfZ4kA1$gD}`6d0vfGYR$Bf1hipHG(&R&pa&4Xchu9%z?QC|6>#dc!U&w zx4Dvskj78e(mlc%{-%bP0sgDCbagq*IXZaJicZnUTDI@FNM{eL?hzy)JjZze7E?^` z|B1`9>dz8|i06My?bL!Wz2H3nz=xC!n7LHL%eefJ8$3!8*5k71tqSt94mYonj};D; z5^`CxE-$kkDka1)3iyb;ti%8KghpJ{FsXvhq_K0Mum*0DsZegO573yWeb`IX&<9*siGnz!;(f zHYMX6B2L$V4+-&8SLlM7jrJom{{;4u-_PuObmK=HhccR5-!gFHufFZG0QB}Vn}8b_ zO!$cBwYd-xg=9keJUk9V_I5v zlt&#~&AuY}VlYmS_n@m zm#!VDqV$)fNnj(7X}{??ipmP_&#WF~SOLN&Q@5%_T*=U(96J@$I zlX>!njFVhmt-?mhT>06w!u3BQB}2-|Dk&`X(Os6wHEAJzC{K|09Y zH)rena4gr{{H&X_l`A)A8;K6{%Ew6{BUP~ODoWQOMH1`hFOXj>cv8^k=N|oG1wD4g z-~!+zPX_Ggo=Xdxx&1X*IRNF@LJAs(#bq%kZow4yLu{5sR5 z+_&Tk{>N3C^MulhG)&3`4czo3lMR%$*=>h7Vac{ON-I)Mv#Acz!;*D-Il=w55Y58@ zvYHp+23(9JUY&z4>|IjUv6%Mz`QKVmPTjwA$p+wW+9CnrURHCOHfp!6Ba;^hQ#ECS zSvsY+Dg{XCdGsa3TDBRu_=Gqct!Nw(N^i|z6?Y-@z>WWLgF(dd23f38x_EO#0jlZ| z3(sULr9JGni!?zTD>zg&Y_-Q#UYk&>n&hQsr#kPtm(+BTwLt6pDZniiW<`Sj%oo%zVD$v z2l$INd`t5QM;5xl^y$Jge2 z;l|hU9*=QC!=(15%VZs``I`&6Yzo>LUQ7$tbDr)FcYTthwBif0naqO$83f_H$_2p9 zmm1=Ypf_{tYN0NXxr&9QUi_nop3dKD-QYmehdNT$yaETfPNpN_PPm&*xXIRRQaegm zSFy}7nLmbkWB{LA^wv-cFoIi;EkqEG5M~O|WU80M8{J4Ii^DW>oNdi{Qn*YLKYZjr z$smnhL5eGq zThipH;Mff5f(hLGBiW7tj`LO*?NmqI>ySi>X!BSkRtnzsn&Kr?OrS)vBtovqF>xCI zid(|vx*TkjN+2W4$}Ni12Ej;(WQSyn1qs!EN=R6C zluH%osGt`vOfn^p%#~^~OD@ZM4oOIHGF!UH=dw*ckpu_jeV+7@)v`fel&8ga43?Sl zg{+n5WV~h9<(8sy>k%b|%nQ^?-k0O@p45_MlH0*_4wB) z_Dz%mG@49PK-hlN848fnitP7alJB@c7o`T_@vNGi` zv?9e@Wb-A3y=X-bkjSma1-j9hi|nVSN)!0j6&q4(Td36HJWgnVJyk7wW)+waA1w~ zfNOJ%BQ}p^FVfgd87-HQqJbNW+{a!TSi9{jvTX^J4H-$nfEMmC{&iTmh4uA(^gJrx zFFkZ;Va<@}7d{drgH@I z>E$OHQCx06;w8?(qO2vIBuO%*pX3$aQBgXGOVXr=lyR6XF5OHBr@7SiHi(Rwb>|~Q zqAdd_Q!uPx}Ty$#G#Yp7_o)#?IC|Uv?MSWP7Z_Cs2mNXJ%iBCi>LT1S8 zvh3#UMOh&od~Rm2ERqfKu>4P^%9CC%MX2&%nO3TWY?EW^Esgo!R(mgmCBe8uvx z$rGxmJz@e2z|H_e6T)MBYvbMLwJ1PgMteJK>u{qKAY#qz4${2rx~IXa0#w`>2Lrug z?Wr_j54RtOX~?t0*yQdJ&hn^Lttiu0!9gf(C_tU;v?4`OllDB!(lF2P3zcj{jYfn7 zL~9(!RRE6BSm{DfU0Ybqu2m`)JXK3`+NRHn*26xtz%mO|bS;247Lg^G!Dt+E=ww!fMJu2Gn_j*t_>r2+co#E+#6RaATR?mO&fHAk6sFLGLID=fT52uaaD6USm^^7=gszo6>JWY- z#vP{0e5wW(0t-#tFjSdsvQOe9S-x^qjCaeA5+UKTOl~;L?viard;Cc*ILw}tRT6b` z{(CuYw65Zkvr$-LBtzCqszk}boB!`SkQH*{=Ip=nqd1M#a!PW`SwFMeWS8WU3|S}n zsjg2QZ2nJTN~$D7}J#gO63NzO(Y370yuR1#&e zw3PQ8?9>XfT#)yrfqd#<*OidZrLxSF((;Lvb+8jZlJ@eBq{?gw4a68QCd*wGvnx%q z)t=5u^H?i;NvNsihW=YXUBW*D0E+`Wsf8z5WGFyUH0Kg~z4BVam}x2{#2Hz42BoTM zg{}-%Qjl_NJ9r4=bC z&RWvxPb59a@~Pku&vLSvLNqPdL}Mf%bl@M$bm^i_ut%8I#cv<*(J`a96j6ozQ%%7Q zG?BO)N3>Z=Q?A8S0YlH_6`rf#2xNzZ#20ESkiP0&iPifmr@q7`l6Zfn`*fR?L@ zq(^|s#Gz)V1AL|CL~eRtp3~qKHJQhJUR{6uZR^Lc1Z>Hb^5SGRH5|t32s6Q_$z>%D zbH*6#BXIF0cX`o@wps^fFRrkbVqPJkDx4vw<31v7k@#`I2SiZL)WU1#L)^i%s3^u3 z+!`kJ^9YytAInMQ6Y@APF^}BDJEXGGAp&p@KX8TFTxC1OebW4Ea*>>lR^<1o;E>My zrofE$^|6Io)A*4!WYFJ{qH&!c_>Od@d048=11@+_ft3KbnMs5m;FG={9eRtZ3zNF{ zP6|^AbqIhoULz_%*G(7Vd{VEnfen6$qP0i?zVj1)EXy(AFl8KGk#<}qr{gIf`zoB- z0CYzj@iqX1{6-!D7TFfNFY!n~Xu)m(E;Eb}v^UC9TSXd;4p>O}rAVR`+L03$?JYv8 z1lSYQC1<6ySKy|GxWp~_rLs7Uni49hQpk^LUqNz9io{DbhYAcOln<2?gCN8wj&ZlK zB&#^aLW-HY$dFnR?`U<^lx!2;DdS->oRw4wY!sAY=Ks&jKY|q*xGOvSc(`~%CU{}k zzCwCRuK@t|2TY2s%ws<9^(5Mx^!OHt7e<(b{3aaYv;*tDx2Xcu*i>+M)aN*xPBu9m zD?Q`VN&#xmXyAQMl|m^%F8VVVSntEy%F9aN6?&7-4h=dd1pLH88ORL))=|m1&WUr! z#^FC!LbRj38-O!347gHdM?Dn7&BOhaTvo4*W7M)+edh2`*JH?Hx`-v;I zr3)q0X2rS5B*PBdvi_s$znuaU$s}BiBgSU8t-{TdR_j5FR-}kyIc{bUj+mwUfimXP zgn)Yx#50u)09I%wa4c^em=krIP9I~&wsKn%idVS?z%>R2n3Wnsb4$&L@S*@U=M4Yo z(uFV>mr{TXt%$QUuvNI*rUpQHuL=(BXv%tYn6^fhsX|q%QI#s(OAR)ogG&@p)u=SP zxTfbqG~-Oau3R>|t&fdXq{Z^d&zEE|kuZ-64qGWsWf~F^uxj!PZXlDn(%tsrOkXd23Sm1LeM0 zq1GFgN#2S*v|g(;9_O&|$g>JWZ4_Sia|?O_Pjfp*BG)#y4cz_?#h(QoAmW zuYk#fdR1`PM7tmw*)-s^^>XzmLhp9O$`N9Rcz>`tddQXh`eyG105(t_2?#}al`71! zg-LbkjI3+LFl{Mox9bj>P-|~4IaEq0%VuuSmd+%y-8940!A27h{hA>(U=?qhV{0}# zxIz<_@wRCuok&xkQ(`pmF17g)myZn_YEfvb8ZXJqg!*`kj`LK&1=wPEjwApMQWF&u zS>wBo7dyoJgKwjTZ>XgYeKlDC{6W(IH-89E@;_XbZ1YBQE^xuvZB9G-ehzbiE0%2Y zMtL@Ko-6!A9qO{*L&7a2g7W-KDyMFuJz)qhaRX~`ot6wEof9NE67HgzKo-YIqc7s8 z&F)cKLu=^KrnPKqGjT zOKf*!Lq3Cxot#%$XGcG-vya1^|DGHG;37Tsglo^FH%Eu& zphxPWvdpIQ?p`3?kOdBy)u{!cSY&7w>I(wy0~$ho8t|m4`d?aUaA}$-W!%q(R#aOXfu*s{ zLzAq_G0xCNziV%n633@Dov|U?Y33pDH<8MNY(xh;$WMf=fOM%(D%5+HtAcbf3Z&YQ%oaMQei#KGs1LoHpa3QZcZ}tHIJ82n2 zyPwYFrZ3moP9ZByy2)eNG}KCUgubSrd|5kj$YlE5Z)Dq9{_&8BVH9Mg%}TqME?%A4 zw$a~a$?c<}St@OU#&|*~Y<&FPy-tf!zziaYHPN^UgxVW^TA|i(rt%k6g6co?zCP(T zh}zQgAHQ82GR5fV3E!2|U-z*V932!yceJ|HH?iI8e8J0Pu*soLcrO#O8N^j~a*u;? zaDY7w;uxoFG5JtR@~v$*#XI;+8!-WwM`mgR58W>>Uzxgn%QZ~uiCnw~Ok)->op1-% zaV86yOa@cQhXjm{90C9q=~)v`ziZR#PZM9rG)GLF*_05C$0h^d;v*^qCFe>Q12rA^ z+K(@f8 zRhIj@l!panB>1X8}C(%3yHM-w&N1w$ecPanH~uyFj?8EB#*zU`P|SxFXC7>SD+KFUWVQ^;l-<9G-O7}2~)qPED-uYdXV zBa`l)BuJ^8%LvaPw<`H5jxzma)u)L8P$VOF3yxvQ?A3CyDG)tx=n$ z+(a!;T9GbX8JmmwaVF8(i~F*P?Ns9&*5DY;h)@GyH7;I5h+`!%SeH&IKuk3cS7}9x z2*v^*QOm_yq?)65vy{pl(J}x+D0i_69X!BdQ$W}rCT&WD$HM*1y=nk#!L5Ot z7-IsD*G0`^g-OG-KDlS8HI@ayGz!s?!bre~H+qIK%m4gE&@v?kPAo0H69D%M~#<*Z`)>9jY5yF@EYT7XfYB;OYF zr;G`c_VytoECohWg5_qBLcOHXShAQ-VJ4EnOroeu2eZt9ql?$CDhTrem?Q{p{fsM`=eF zOA5nyYv0!9Hxl`kQ!4KY&|00$ldQ_;$-L@FANZVWW_A*x4tCwUxcGr=rjm=2bk(~F zRPxUv3jh}%P*Gp-!Kv!%a)9`s8XZ1;(8DT6x^Y|$-1bONs6aPe{CR+z2M|Zh1mG1v zv)uvAqrZ#o0Jv#EgleHKV1$PvQP_$QBvX;5Bm(RFCOTaLHv7?`3$hpZn>dQnO|8a2 z(Vm?Ez$tp_i&B>flWoBq=dg}5W>p_}F`s%+-U9&Rlr4k=g}QVk(#W<8oZ=F{P|~re z)Je{Akpqq`E9!8N!(8MLEfEHg3T#osz7pF^HP^EquUU*`Dt}5mi@0G*&cEhptbfi` z4wA$eheI%e`P?vxA>-*DxR$Nz&zqzJ03T3Zue6J6ZsWP)A9Ab7(V-KUso3Rdjw)R* z0SOABJWLCoFtY7#`jhO?bhZ`OI7k(ma>mFy?`1p586KdzQBxj5c$3xKM>wGt1t`hL zxLsuE)kYD|SXNNTK=KGj<@m_Y%?@sU30z|mMM%A971 z$C)(t$D1iQ;vfE}3?_Gb@>;ZVjXok1fK+CY9|;QKJj5^jWElhlxb78dZNWMAQo~Lw zQq<$9?xngM*36v{YuS_nL>+c2tw@Nl4#Gbej%Wpkr);}zH86<~^Y1Uf7HW-PA{f~g z&Er%>fr^CcKoi|PfNmrUb37IstcbIjMzP~fSP7^WXCDSckY*}H_6+i0vLaI zAtXpd(u6jIdGt{HoswqRF7mIJ0nl7St#vtS)&EfNAjdgMbFGxn)hk_OD;Leu6=w-& z8R!-C*=SX8$jz7b+R++n9ZLuiyaPt3&c)-@M}kH;37iHn0oe+u(v>;x#c^_L_?u8H z4;!|ls_wZ4{H-Re?v|jI?h)D(p;9!Nt@PlMec99`KhIt|b5t9F*=64Gn|P9KBs*N$ zJ?!QNPnc}dCkb)r)tbQoJ~Q;zP#-_y=VUUT5u}^^nHaiIhY%!a5-I^Q)^_Y00 z2D^b2UiU+bTMM`u?YJPZECb+eB8a0S)sg=Mt<;EFqpDdbeVNg1hTIOJH{^B*-CBG} z6%jM8$v3ja3c<9M4iYcH4ww8a7p0vPvw}aE|4kRCk!AujEBdD@-riFs}2$?b8g zNyX^!@n6^Ra9Mqf3xG5h-hs@QStz9Ef%gm4cVNg=_fiLQ&uCd#mhEcy^)lft8on=g(>=&8=F z;8BC+>izBFaUWxToEAGxZjW0}D<*Y*CMjOum5%(Ot=&5jn!}!G+!p1TsBNBI*76-tO-jN2bhJoB&yvC_3R`>pfGH?%rxop@npR3UM_1c#IjG%) zOR@^LO(m&g?7Rf)QO)(D0F^eCp`Wm9zEQv@+(%OyIGiAz_?REr#9MdB@(!aZdjW9s zEqa#GBzydl@scx0PQN&@(D36bcc@@m(E=nhouap`zA%NW0Hm^+18NXO(|YnyoZ8zw5p`Hx#lx+<)&v&mgBf0He2l zKoZ$}PXc%G80&#Ge8GCYp(jzdt+upQqi@gX0h^EK&&;kmTXGJ$qc4Zdc%qjp8s%jv zE$#-B{2mtFxdxo$aoesez+5tcHB`LCAzPX@Ol23_DTnw}O1MfBi;q&>M*?jnn_g7o zFsFIMhn%nqc!?@T)_E^o6`tcs$`S6kYz_H}4B#~V2|-9;1CR!m!ZqlYOIOZVi~!e| zqq;#9-I+DOHuqMt$#(78C$k$Qk4A8Gclw)s$oj zX)5XZJKk_x4gyE1XS3?&a|Oua4dQHjEZ*@wFF+>-(U_v*@Hq`jPGm| zpxPF#NLgGq3Q&3eAdLiyP@7J4p`znX3-fr98e8W~21{8?s!4?IPA`-02>bk|#uQ?t zW(ORkBN2M24?i}g@)Wr<_?_^NsZR|c29N4>lsy+~vq)h#bcrWXb1J`PzF+BoL;R7z;Z@2sL5&HM(c z^3vSnFpj1tI{-+*MJn%GujCZY0bBg=XX1E93o~9~q6VUKF@Q+}C&`^cCIx`8lT?c& zjK@^c!S{|y{%!aLIKw#F(URV*Bnf~EOz=2fOEZGKq_LeL6!%McC`LV=U^s24YV&zE z;UfPqfK6VN5;W9ms5r)Mv#v0finO9Djj2hgTjY^eWfFV&f|?$S&{wNGEVYi|63irn zY~~T?Xae+B3SJ7cJj6o+>Ci)fS@3oih{TyI=EG_ zRj2Nb?Yy$G*LsV zh^}7>}31@FD#f&hjyGiln&=@*G zy5D)5aAa5>T05{kp%FfE)DE(kNj*55D7@7FxD-+}h5o~~RR$>*6Eu3%b?QNxvs~hQ71FX|O1 z6o(oqrN{tF{X$l;q#`9-Q>sjvN;w0yd&B}`RcW)(>sDycDy4rhlYG^eoFHkA+dUwq zo0P>$9jH{51Efsx*G75Iul-KSwvKYO=^_Z8benmA5kA*c_2tfYI(_s+ZnSgK7q7kj z?t*5(FxNRpF=+OCH>lTFsS})Sx*`!7@A<8{-UNeO;tbQ!;z<)7Xq}t=K*om`Q>jX= zN=e0Hx^xO+-HA%+&}yAlt=fGU#ZGs=SxUjZuJ^jWhPc{!DzHL_@!s|;w`yw|+yCt*v^D1J|v+V39uDdB{0N;o2n35o7O zZ)W&W*x0?&@U3up{-)){J}rXDoqAhlw0oo8&ZaV45jKU@;hB&NA-`#%uAdc;n|k(W zJsmC#)&2Y{P7IHSOwYE)zYRq_!~HqopRW8TI~zOyr6KaYYtQX<^E@s85bM9Sg?n~5 zpNaIkI$jhW3T^q3|BbCh8yvoopWHqYM)%po`t_}atPb{aCWN1d?}pjkw_c0G`Td*- zW5aFv)AjLiW+?9GJ3T4Cr1albgl~naUJvB{&%ODk{GE(@cOkbGU;o3Ca?xIA*Kb3* zr*wCHKr8#ua79=cvfYh_mxr=Y8~!!q^1E3E^!lHCf9MQz!#{+)kPA=t`~4go?hY^Jo4ERW$}%T(^{ia>{&}gB!c}kGFy;n3pV%}lJDpTl?3LkD z!kA?sSYe}L2a2M=I5ie)6l^cOcdvQ60B1T^jS=4RxKz*e ztkdpPWt8(AXn~v_f)-uUHkjxD5tTN2Lt0EUo{DCqb6{$lt;=*YKD+#G%yHssIA2lJob z?==^!+IlpXf70-Up7Z#W@SQL_j1Q4uw`6?y*YIXo7arO{A3!90w!1Ckt$YJyVJHqh zAVuMf@IuJ;^uK;Bl=oVV`^LI!XIyL75}P5jOUboF=Z=_Z$TASD)2z%mQN)clLbH{+ z^p#a_iJUg~_=%T9B^_u#B_h`Qsr7)PT%-5T0zVS{z9QG z9`TSaFxH=&s!EgpvJ#(ho|%qvsQuNdP$YqvxGW$lt`Ip8aiPyU!SPmU0IljwSE5pr zIWo4l#RkKS7c)|!i%imJk*xj=ajw60xCmJ64nNk~Q?|3WjJ`Dbo2Xq)JZ{%z>th$5 zdVga4T#z-#7HJnKM8r(hs{Pa&+giIdK#Sj5>0k%y@|q^VehycR29M|f9O-am)q4XB z@^?ygY13+xjhdunb!u|A1tP{f*ci3$@+yd_@=Zlnx=%MRdBv#;j55xA{aNTi2biMV zmt-w4Op&zy?C+~iF%huHT|Hw3rA|s8aH+fO2JwX5AyJ?E>WM$jO?V#U%+n@mwlXkT zg#m6;FKb64iWDQ|J?(%4%n%c-wpaiaMjL`<=GzQPtx~L1k!rooH`f9&NBg>An%yTN zAfsIlC5DN~f_Jy(L>(U*y1^}9HiEGgRHl$ zxN}`*l8m_?^Y51J@H@EFF$zH1ANjm6Q>@G+QACxA>!!(T zW+``o{Y2$JiCRgE1i_Pr`Gfl`)t`ikF81f@0F8d(|9V$970zrMe|6n0vUZzzmECId z)7Ko5OkVz^wV>I5X!I4wr~-riFJt_|A2rzCJnxyR5fA}6jXgygVVZ=TrB(=fi-=32 z(^^>&0F4?PBx-^y)krE-V-kof*97SBq%SI$G}G^GFwAf>&9K;YHlWiT%C+cEjpJQx zhDyM@?suz|bgOkr*Z6PM-6m^yx_1xc`g5*mp5A8{;x*yb@IT%UcZAP`(hq!}+#UJn zfNzE`~ zvC;zb%r(!uCW}gW$vbrGRAGi9C8nEgqAJYuyoHw7kug5+Z=7xbV7cG;p?TWrcC=aR zN1PFPG-7whO?xWCu6gM6Ud#B=aB}!bXbAtKG5kE77Ha0wl;3bF9N^ZU-$ZT%eS)A9O7&t}6V;b-9+;kZz- z!=<5Qm>KR2t3tPs3iHBWh1zXeHoyDI^^>o+ClXKFLst9zv>yz<plyx|Kb zD+V<#a-f&o>rJitlkp!`JKAh@$`xwMU-th~vu%|dEQT(f`pQY!BB4x!m*v2EEh<$Q zD9~e}r|oBxcRk`g3oMn>zXGG2>`XJn0S%sVw%G%aO z>y<0FpM(@xX01^Q6-(-*+gkJR2aBz<#g0ud%ULF?0knF>FFj=?J;s}1^xt>hp~;?d z%^uFbjkxN+{-V!*teS99_+@AeJ@T)oxgyLAu|LtjSs2RxL`Tf5eA`l{-yhL=RKC+X z7w!thJvU4Db-(+#cAG79)gRb*^u=&z*wAyMlMAnhe-1~6!fiIGe|yc1XNuT^;@8;2 zR^4<~=@r#~{q(?vGGL>7ykwDCj*ZQpcJ4$C|Ht=U?ne@qe+$ZAn}^ zJX zI?bOsOflWk)_B!J-u9L)KCCJOBqVKCZ(9jh8Evsep3nq{iOBdc>Wp`clhhfkTmH|w z-}Bo%G_Q2L36E9($iw!6$Pes2$wL~yUq9;!>*!HoqH`Q!sulgA#I1!lSZl9{+-z?uFI#3JB7cn`gO~%-1AsZ;2{2$dM*F*fiA=fG#cOc*(<-=+aw-Sz+9QL0>C= zNteAOa+|$v!^q54EuVdM&^y}cQEG}q%raGtBt1I3ZL#_0SgT#DoINL@LZw3-WrWE_ zOVF*=dQY3{H46m2m6`0&^}~KveXS1of4Fe4`Lk_bu0QSh$TGbZnB)MbIMNX1dRuFW zRtr6AwH6y}wmZd?8=%r?#~EX2&kYj0f!!UKaUBACGK& z@bOFk`TZ$RSg)^Y`#Z%1BORnzZz-)hEcUW`E!JqzvZ`*qd7dCtcm@*^Lg6~_5_Ai?Gqq=u=Zo5XEFw4hcoh$By>|5(uMVu&WLr^#O)=60C#sfIENWXh%{Ex2QChG? zOTK^|+NDIqM8%Zki&U&gfrJVJRjE=Vs;_kZ>t6G`H5RJ3$@a>Oba>nF1<53|=@Y^fz65#*mg@aoT%N)Pt>W0#nt&#iARhEtByH5_!re(Y#5~FWP_Ukt+r@SglF$ z;lw1x#YFSJ20>1cm6p~e=fjApR%?LT)TC#l%cBopdB5NjC*l+A=0lB29r^ybn@4Q2 z*h($-tV*MdQKoG9p#Lm?CcVD>IT`)iH&lL*AqjP%bo0@TXSW}cDqaD zBBG)S)fjA4wmK8tQubQSy_L^*Z7y1)(THAS2~dA@ZnR^Am$>ynX{6Y|L+(tW*)iYQPhnJw8|v8lABVqJ7odP`zW?7j4S zFZ&dVTeYQ_GMq;_GMq;_7!elK3Vzy0HXtc08?J5ssI2007*qoM6N<$ Eg0IXp;{X5v literal 0 HcmV?d00001 diff --git a/docs/_templates/brand.html b/docs/_templates/brand.html index c48d7eb7..ec1816ee 100644 --- a/docs/_templates/brand.html +++ b/docs/_templates/brand.html @@ -1,4 +1,5 @@ -

About Flask-OAuthlib

+ +

Flask-OAuthlib

Flask-OAuthlib is a replacement for Flask-OAuth. It depends on the oauthlib module.

{%- block footerinner %} diff --git a/docs/conf.py b/docs/conf.py index 79eeac8d..4d82d38b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -116,7 +116,7 @@ # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +html_logo = 'flask-oauthlib.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 From a99663e2c611553370b150d75291b3fa72fcc2ea Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 24 Dec 2013 16:19:31 +0800 Subject: [PATCH 014/279] Documentation of example on oauth servers. --- docs/oauth1.rst | 9 ++++++++- docs/oauth2.rst | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/oauth1.rst b/docs/oauth1.rst index 0f865162..5a6e7eb7 100644 --- a/docs/oauth1.rst +++ b/docs/oauth1.rst @@ -21,7 +21,6 @@ Like any other Flask extensions, we can pass the application later:: To implemente the oauthorization flow, we need to understand the data model. - User (Resource Owner) --------------------- @@ -473,3 +472,11 @@ rename it to other names, for exmaple:: def me(data): user = data.user return jsonify(email=user.email, username=user.username) + + +Example for OAuth 1 +------------------- + +Here is an example of OAuth 1 server: https://github.com/lepture/example-oauth1-server + +Also read this article http://lepture.com/en/2013/create-oauth-server. diff --git a/docs/oauth2.rst b/docs/oauth2.rst index e681f898..cb0a249e 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -476,3 +476,11 @@ rename it to other names, for exmaple:: def me(data): user = data.user return jsonify(email=user.email, username=user.username) + + +Example for OAuth 2 +------------------- + +Here is an example of OAuth 2 server: https://github.com/lepture/example-oauth2-server + +Also read this article http://lepture.com/en/2013/create-oauth-server. From 7be617da5cd296c76835a42ac5028e98a7a4c139 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 3 Jan 2014 16:40:55 +0800 Subject: [PATCH 015/279] Version bump 0.4.2 --- docs/changelog.rst | 12 ++++++++++++ flask_oauthlib/__init__.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 01fbe0c1..ae29d07d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,18 @@ Changelog Here you can see the full list of changes between each Flask-OAuthlib release. +Version 0.4.2 +------------- + +Released on Jan 3, 2014 + +Happy New Year! + +- Add param ``state`` in authorize method via `#63`_. +- Bugfix for encoding error in Python 3 via `#65`_. + +.. _`#63`: https://github.com/lepture/flask-oauthlib/issues/63 +.. _`#65`: https://github.com/lepture/flask-oauthlib/issues/65 Version 0.4.1 ------------- diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index bb71ec49..77c481aa 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -11,7 +11,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = "0.4.1" +__version__ = "0.4.2" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' From 4fcb649fe3fc50d9fc27f00ce9e48a333f7fb648 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 7 Jan 2014 11:52:10 +0800 Subject: [PATCH 016/279] Update copyright --- LICENSE | 2 +- docs/conf.py | 2 +- flask_oauthlib/__init__.py | 2 +- flask_oauthlib/client.py | 2 +- flask_oauthlib/contrib/__init__.py | 2 +- flask_oauthlib/provider/__init__.py | 2 +- flask_oauthlib/provider/oauth1.py | 2 +- flask_oauthlib/provider/oauth2.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/LICENSE b/LICENSE index 12955663..5c88f320 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013, Hsiaoming Yang. +Copyright (c) 2013 - 2014, Hsiaoming Yang. All rights reserved. diff --git a/docs/conf.py b/docs/conf.py index 4d82d38b..3da329da 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,7 +46,7 @@ project = u'Flask-OAuthlib' import datetime -copyright = u'%i, Hsiaoming Yang' % datetime.datetime.utcnow().year +copyright = u'2013 - %i, Hsiaoming Yang' % datetime.datetime.utcnow().year # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index 77c481aa..2eb4b248 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -7,7 +7,7 @@ remote OAuth enabled applications, and also helps you creating your own OAuth servers. - :copyright: (c) 2013 by Hsiaoming Yang. + :copyright: (c) 2013 - 2014 by Hsiaoming Yang. :license: BSD, see LICENSE for more details. """ diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 8027d734..77430db1 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -5,7 +5,7 @@ Implemnts OAuth1 and OAuth2 support for Flask. - :copyright: (c) 2013 by Hsiaoming Yang. + :copyright: (c) 2013 - 2014 by Hsiaoming Yang. """ import logging diff --git a/flask_oauthlib/contrib/__init__.py b/flask_oauthlib/contrib/__init__.py index bcfa8607..f1c7842f 100644 --- a/flask_oauthlib/contrib/__init__.py +++ b/flask_oauthlib/contrib/__init__.py @@ -5,5 +5,5 @@ Contributions for Flask OAuthlib. - :copyright: (c) 2013 by Hsiaoming Yang. + :copyright: (c) 2013 - 2014 by Hsiaoming Yang. """ diff --git a/flask_oauthlib/provider/__init__.py b/flask_oauthlib/provider/__init__.py index 050a2655..1b0aee04 100644 --- a/flask_oauthlib/provider/__init__.py +++ b/flask_oauthlib/provider/__init__.py @@ -5,7 +5,7 @@ Implemnts OAuth1 and OAuth2 providers support for Flask. - :copyright: (c) 2013 by Hsiaoming Yang. + :copyright: (c) 2013 - 2014 by Hsiaoming Yang. """ # flake8: noqa diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index bcfe9e48..f95547c8 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -5,7 +5,7 @@ Implemnts OAuth1 provider support for Flask. - :copyright: (c) 2013 by Hsiaoming Yang. + :copyright: (c) 2013 - 2014 by Hsiaoming Yang. """ import logging diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 831cc447..eafa9970 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -5,7 +5,7 @@ Implemnts OAuth2 provider support for Flask. - :copyright: (c) 2013 by Hsiaoming Yang. + :copyright: (c) 2013 - 2014 by Hsiaoming Yang. """ import os From bd65002a968baddc8e699b6445ea6baa28c66b65 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 7 Jan 2014 23:12:44 +0800 Subject: [PATCH 017/279] Add @tonyseek into author list. --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 741947a2..c6c60c22 100644 --- a/AUTHORS +++ b/AUTHORS @@ -2,3 +2,4 @@ - Randy Topliffe https://github.com/Taar - Mackenzie Blake Thompson https://github.com/flippmoke - Ib Lundgren https://github.com/ib-lundgren +- Jiangge Zhang https://github.com/tonyseek From 49d634bfba516015d11cf581134bb1b228d92cb3 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 7 Jan 2014 23:21:28 +0800 Subject: [PATCH 018/279] Douban fixed its bug! --- example/douban.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/example/douban.py b/example/douban.py index ecafa2a0..70865afc 100644 --- a/example/douban.py +++ b/example/douban.py @@ -17,8 +17,6 @@ access_token_url='https://www.douban.com/service/auth2/token', authorize_url='https://www.douban.com/service/auth2/auth', access_token_method='POST', - # douban's API is a shit too!!!! we need to force parse the response - content_type='application/json', ) From 292d2738ec5a41594c395ab4a12098875dabfa2b Mon Sep 17 00:00:00 2001 From: Shenghao Huang Date: Wed, 8 Jan 2014 15:35:12 +0000 Subject: [PATCH 019/279] Branch:dec - adding return statement to all the decorator func --- flask_oauthlib/provider/oauth1.py | 11 +++++++++++ flask_oauthlib/provider/oauth2.py | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index f95547c8..627cf5ff 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -151,6 +151,7 @@ def limit_client_request(): track_request(client) """ self._before_request_funcs.append(f) + return f def after_request(self, f): """Register functions to be invoked after accessing the resource. @@ -165,6 +166,7 @@ def valid_after_request(valid, oauth): return valid, oauth """ self._after_request_funcs.append(f) + return f def clientgetter(self, f): """Register a function as the client getter. @@ -190,6 +192,7 @@ def get_client(client_key): return client """ self._clientgetter = f + return f def tokengetter(self, f): """Register a function as the access token getter. @@ -210,6 +213,7 @@ def get_access_token(client_key, token): return AccessToken.get(client_key=client_key, token=token) """ self._tokengetter = f + return f def tokensetter(self, f): """Register a function as the access token setter. @@ -243,6 +247,7 @@ def save_access_token(token, request): - request_token: Requst token for exchanging this access token """ self._tokensetter = f + return f def grantgetter(self, f): """Register a function as the request token getter. @@ -263,6 +268,7 @@ def get_request_token(token): return RequestToken.get(token=token) """ self._grantgetter = f + return f def grantsetter(self, f): """Register a function as the request token setter. @@ -281,6 +287,7 @@ def save_request_token(token, request): return data.save() """ self._grantsetter = f + return f def noncegetter(self, f): """Register a function as the nonce and timestamp getter. @@ -301,6 +308,7 @@ def get_nonce(client_key, timestamp, nonce, request_token, return Nonce.get("...") """ self._noncegetter = f + return f def noncesetter(self, f): """Register a function as the nonce and timestamp setter. @@ -317,6 +325,7 @@ def save_nonce(client_key, timestamp, nonce, request_token, if you put timestamp and nonce object in a cache. """ self._noncesetter = f + return f def verifiergetter(self, f): """Register a function as the verifier getter. @@ -335,6 +344,7 @@ def load_verifier(verifier, token): return data """ self._verifiergetter = f + return f def verifiersetter(self, f): """Register a function as the verifier setter. @@ -356,6 +366,7 @@ def save_verifier(verifier, token, *args, **kwargs): return data.save() """ self._verifiersetter = f + return f def authorize_handler(self, f): """Authorization handler decorator. diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index eafa9970..878a1502 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -182,6 +182,7 @@ def limit_client_request(): track_request(client) """ self._before_request_funcs.append(f) + return f def after_request(self, f): """Register functions to be invoked after accessing the resource. @@ -196,6 +197,7 @@ def valid_after_request(valid, oauth): return valid, oauth """ self._after_request_funcs.append(f) + return f def clientgetter(self, f): """Register a function as the client getter. @@ -225,6 +227,7 @@ def get_client(client_id): return client """ self._clientgetter = f + return f def usergetter(self, f): """Register a function as the user getter. @@ -238,6 +241,7 @@ def get_user(username=username, password=password, return get_user_by_username(username, password) """ self._usergetter = f + return f def tokengetter(self, f): """Register a function as the token getter. @@ -264,6 +268,7 @@ def bearer_token(access_token=None, refresh_token=None): return None """ self._tokengetter = f + return f def tokensetter(self, f): """Register a function to save the bearer token. @@ -288,6 +293,7 @@ def set_token(token, request, *args, **kwargs): client object. """ self._tokensetter = f + return f def grantgetter(self, f): """Register a function as the grant getter. @@ -303,6 +309,7 @@ def grant(client_id, code): - delete: A function to delete itself """ self._grantgetter = f + return f def grantsetter(self, f): """Register a function to save the grant code. @@ -314,6 +321,7 @@ def set_grant(client_id, code, request, *args, **kwargs): save_grant(client_id, code, request.user, request.scopes) """ self._grantsetter = f + return f def authorize_handler(self, f): """Authorization handler decorator. From 09ce0f9600de4d3e1aad5e1237b2aa0b9a04ee30 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 5 Feb 2014 11:24:41 +0800 Subject: [PATCH 020/279] Validation right for scopes on oauth2. #72 --- flask_oauthlib/provider/oauth2.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index eafa9970..f26d9808 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -763,11 +763,9 @@ def validate_response_type(self, client_id, response_type, client, request, def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): """Ensure the client is authorized access to requested scopes.""" - if set(client.default_scopes).issuperset(set(scopes)): - return True if hasattr(client, 'validate_scopes'): return client.validate_scopes(scopes) - return True + return set(client.default_scopes).issuperset(set(scopes)) def validate_user(self, username, password, client, request, *args, **kwargs): From 2d1567c88f9047ab4cb9b50d43dcab0d10b7276a Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 5 Feb 2014 11:25:16 +0800 Subject: [PATCH 021/279] Fix test cases for validation #72 --- tests/oauth2/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index ee70fb90..b8d06356 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -29,7 +29,7 @@ class Client(db.Model): nullable=False) client_type = db.Column(db.String(20), default='public') _redirect_uris = db.Column(db.Text) - default_scope = db.Column(db.Text) + default_scope = db.Column(db.Text, default='email address') @property def user(self): From b915eb7423022218ead21f0ae4363e0779f88f48 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sun, 9 Feb 2014 21:00:35 +0800 Subject: [PATCH 022/279] Fix for oauthlib 0.6.1, add client_authentication_required https://github.com/idan/oauthlib/commit/dff8e6d170f8028c9169bde1d208c1cccbea5aae --- flask_oauthlib/provider/oauth2.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index e4884efc..3fd8ac10 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -463,6 +463,9 @@ def __init__(self, clientgetter, tokengetter, grantgetter, self._grantgetter = grantgetter self._grantsetter = grantsetter + def client_authentication_required(self, request, *args, **kwargs): + return request.grant_type in ('password', 'refresh_token') + def authenticate_client(self, request, *args, **kwargs): """Authenticate itself in other means. From 504a2ba63d8bf21d3233675090798105da59e1e9 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sun, 9 Feb 2014 21:10:54 +0800 Subject: [PATCH 023/279] Handle empty response of json. close #69 --- flask_oauthlib/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 77430db1..6dc7bd35 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -115,6 +115,8 @@ def parse_response(resp, content, strict=False, content_type=None): ct, options = parse_options_header(content_type) if ct in ('application/json', 'text/javascript'): + if content == '': + return {} return json.loads(content) if ct in ('application/xml', 'text/xml'): From bce32bab4adf3956af8ac8f4332a48998f25cbf0 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sun, 9 Feb 2014 21:16:01 +0800 Subject: [PATCH 024/279] Prepare for the next release of 0.4.3 --- docs/changelog.rst | 12 ++++++++++++ flask_oauthlib/__init__.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ae29d07d..b5b0e814 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,18 @@ Changelog Here you can see the full list of changes between each Flask-OAuthlib release. +Version 0.4.3 +------------- + +Release date to be decided. + +- OAuthlib released 0.6.1, which caused a bug in oauth2 provider. +- Validation for scopes on oauth2 right via `#72`_. +- Handle empty response for application/json via `#69`_. + +.. _`#69`: https://github.com/lepture/flask-oauthlib/issues/69 +.. _`#72`: https://github.com/lepture/flask-oauthlib/issues/72 + Version 0.4.2 ------------- diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index 2eb4b248..2675ad10 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -11,7 +11,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = "0.4.2" +__version__ = "0.4.3" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' From e6c5d75679d7f920a3b068b461e8a47aa4e11970 Mon Sep 17 00:00:00 2001 From: Scott Vitale Date: Tue, 11 Feb 2014 19:12:26 -0700 Subject: [PATCH 025/279] Add the ability to pass additional URL parameters to the authorize() call --- flask_oauthlib/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 6dc7bd35..7f10b12a 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -440,7 +440,7 @@ def request(self, url, data=None, headers=None, format='urlencoded', ) return OAuthResponse(resp, content, self.content_type) - def authorize(self, callback=None, state=None): + def authorize(self, callback=None, state=None, **kwargs): """ Returns a redirect response to the remote authorization URL with the signed callback given. @@ -449,6 +449,7 @@ def authorize(self, callback=None, state=None): :param state: an optional value to embed in the OAuth request. Use this if you want to pass around application state (e.g. CSRF tokens). + :param kwargs: add optional key/value pairs to the query string """ if self.request_token_url: token = self.generate_request_token(callback)[0] @@ -459,6 +460,8 @@ def authorize(self, callback=None, state=None): assert callback is not None, 'Callback is required OAuth2' params = dict(self.request_token_params) or {} + params.update(**kwargs) + client = self.make_client() if 'scope' in params: From 16f883ded7e0a00a02458d5d3e60f7e96d8fe41a Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 18 Feb 2014 12:00:27 +0800 Subject: [PATCH 026/279] Release 0.4.3 --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b5b0e814..29acb32f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,7 +8,7 @@ Here you can see the full list of changes between each Flask-OAuthlib release. Version 0.4.3 ------------- -Release date to be decided. +Released on Feb 18, 2014 - OAuthlib released 0.6.1, which caused a bug in oauth2 provider. - Validation for scopes on oauth2 right via `#72`_. From e32e35ef985970a46d85eeccde2d6296b44f176a Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sat, 8 Mar 2014 11:04:55 +0800 Subject: [PATCH 027/279] Confidential client can obtain Authorization Code grant flow. https://github.com/lepture/flask-oauthlib/issues/82 --- flask_oauthlib/provider/oauth2.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 3fd8ac10..4acd1f48 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -518,12 +518,6 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): # attach client on request for convenience request.client = client - - # authenticate non-confidential client_type only - # most of the clients are of public client_type - if client.client_type == 'confidential': - log.debug('Authenticate client failed, confidential client.') - return False return True def confirm_redirect_uri(self, client_id, code, redirect_uri, client, From 84f81069d2354e707e8a4ab6a643c2c520aa3264 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Sun, 9 Mar 2014 13:04:02 -0400 Subject: [PATCH 028/279] Fix issue where a different method was used for signing OauthV1 requests than the method used to request them. --- flask_oauthlib/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 7f10b12a..d3ce2704 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -557,7 +557,8 @@ def handle_oauth1_response(self): ) resp, content = self.http_request( - uri, headers, to_bytes(data, self.encoding) + uri, headers, to_bytes(data, self.encoding), + method=self.access_token_method ) data = parse_response(resp, content) if resp.code not in (200, 201): From 381f4bcec2905392d430e3676c3ae4cfaf67e907 Mon Sep 17 00:00:00 2001 From: Stian Prestholdt Date: Mon, 17 Mar 2014 15:55:39 +0100 Subject: [PATCH 029/279] Fix client_credentials logic in validate_grant_type request.user was never set since grant_type was valid in the implemented allowed_grant_types method. This also updates the Client-model in oauth2-test to include allowed_grant_types to be sure that this fixes the bug. --- flask_oauthlib/provider/oauth2.py | 11 ++++++----- tests/oauth2/server.py | 5 +++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 4acd1f48..f226afd6 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -705,15 +705,16 @@ def validate_grant_type(self, client_id, grant_type, client, request, 'client_credentials', 'refresh_token'): return False - if hasattr(client, 'allowed_grant_types'): - return grant_type in client.allowed_grant_types + if (hasattr(client, 'allowed_grant_types') and + grant_type not in client.allowed_grant_types): + return False if grant_type == 'client_credentials': if hasattr(client, 'user'): request.user = client.user - return True - log.debug('Client should has a user property') - return False + else: + log.debug('Client should have a user property') + return False return True diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index b8d06356..ef5155af 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -51,6 +51,11 @@ def default_scopes(self): return self.default_scope.split() return [] + @property + def allowed_grant_types(self): + return ['authorization_code', 'password', 'client_credentials', + 'refresh_token'] + class Grant(db.Model): id = db.Column(db.Integer, primary_key=True) From 107e6b9d1849ee493d27cfebe25d333e7be74d1f Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 18 Mar 2014 22:34:03 +0800 Subject: [PATCH 030/279] Fix code style with less ident --- flask_oauthlib/provider/oauth2.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index f226afd6..796063ff 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -701,20 +701,23 @@ def validate_grant_type(self, client_id, grant_type, client, request, log.debug('Password credential authorization is disabled.') return False - if grant_type not in ('authorization_code', 'password', - 'client_credentials', 'refresh_token'): + default_grant_types = ( + 'authorization_code', 'password', + 'client_credentials', 'refresh_token', + ) + + if grant_type not in default_grant_types: return False - if (hasattr(client, 'allowed_grant_types') and - grant_type not in client.allowed_grant_types): + if hasattr(client, 'allowed_grant_types') and \ + grant_type not in client.allowed_grant_types: return False if grant_type == 'client_credentials': - if hasattr(client, 'user'): - request.user = client.user - else: + if not hasattr(client, 'user'): log.debug('Client should have a user property') return False + request.user = client.user return True From 90b08a734e134bb392a2942eb9559403d019d188 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 26 Mar 2014 01:52:36 +0800 Subject: [PATCH 031/279] Remove `oauth` param in `require_oauth` decorator. Clean OAuth2 Provider. --- flask_oauthlib/provider/oauth2.py | 7 ++++--- tests/oauth2/server.py | 11 +++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 796063ff..27cc8aa4 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -65,8 +65,8 @@ def create_app(): @app.route('/api/user') @oauth.require_oauth('email', 'username') - def user(oauth): - return jsonify(oauth.user) + def user(): + return jsonify(request.oauth.user) """ def __init__(self, app=None): @@ -440,7 +440,8 @@ def decorated(*args, **kwargs): if not valid: return abort(403) - return f(*((req,) + args), **kwargs) + request.oauth = req + return f(*args, **kwargs) return decorated return wrapper diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index ef5155af..a758f568 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -257,22 +257,25 @@ def access_token(): @app.route('/api/email') @oauth.require_oauth('email') - def email_api(oauth): + def email_api(): + oauth = request.oauth return jsonify(email='me@oauth.net', username=oauth.user.username) @app.route('/api/client') @oauth.require_oauth() - def client_api(oauth): + def client_api(): + oauth = request.oauth return jsonify(client=oauth.client.name) @app.route('/api/address/') @oauth.require_oauth('address') - def address_api(oauth, city): + def address_api(city): + oauth = request.oauth return jsonify(address=city, username=oauth.user.username) @app.route('/api/method', methods=['GET', 'POST', 'PUT', 'DELETE']) @oauth.require_oauth() - def method_api(oauth): + def method_api(): return jsonify(method=request.method) return app From 49ea96ca4966eb1d3952482ab35cb93eb1805f89 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 26 Mar 2014 01:55:21 +0800 Subject: [PATCH 032/279] Remove `oauth` param in `require_oauth` decorator. Clean OAuth1 Provider --- flask_oauthlib/provider/oauth1.py | 7 ++++--- tests/oauth1/server.py | 8 +++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index 627cf5ff..17e853b6 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -49,8 +49,8 @@ def create_app(): @app.route('/api/user') @oauth.require_oauth('email', 'username') - def user(oauth): - return jsonify(oauth.user) + def user(): + return jsonify(request.oauth.user) """ def __init__(self, app=None): @@ -504,7 +504,8 @@ def decorated(*args, **kwargs): return abort(403) # alias user for convenience req.user = req.access_token.user - return f(*((req,) + args), **kwargs) + request.oauth = req + return f(*args, **kwargs) return decorated return wrapper diff --git a/tests/oauth1/server.py b/tests/oauth1/server.py index ef8421b7..51c5d084 100644 --- a/tests/oauth1/server.py +++ b/tests/oauth1/server.py @@ -239,17 +239,19 @@ def access_token(): @app.route('/api/email') @oauth.require_oauth('email') - def email_api(oauth): + def email_api(): + oauth = request.oauth return jsonify(email='me@oauth.net', username=oauth.user.username) @app.route('/api/address/') @oauth.require_oauth('address') - def address_api(oauth, city): + def address_api(city): + oauth = request.oauth return jsonify(address=city, username=oauth.user.username) @app.route('/api/method', methods=['GET', 'POST', 'PUT', 'DELETE']) @oauth.require_oauth() - def method_api(oauth): + def method_api(): return jsonify(method=request.method) return app From 8b0d0459fe3b93a2cea52738d0d192be3250c8e9 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 26 Mar 2014 10:28:27 +0800 Subject: [PATCH 033/279] Concept for session authentication and oauth authentication. https://github.com/lepture/flask-oauthlib/issues/87 --- flask_oauthlib/provider/oauth1.py | 3 +++ flask_oauthlib/provider/oauth2.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index 17e853b6..23a8b437 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -491,6 +491,9 @@ def decorated(*args, **kwargs): for func in self._before_request_funcs: func() + if hasattr(request, 'oauth') and request.oauth: + return f(*args, **kwargs) + server = self.server uri, http_method, body, headers = extract_params() valid, req = server.validate_protected_resource_request( diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 27cc8aa4..32bc5574 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -429,6 +429,9 @@ def decorated(*args, **kwargs): for func in self._before_request_funcs: func() + if hasattr(request, 'oauth') and request.oauth: + return f(*args, **kwargs) + server = self.server uri, http_method, body, headers = extract_params() valid, req = server.verify_request( From 0f96b5eeaa99f2a6f42e44426dfac48328837382 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 2 Apr 2014 15:18:41 +0800 Subject: [PATCH 034/279] Request token params for OAuth1. https://github.com/lepture/flask-oauthlib/issues/91 --- flask_oauthlib/client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index d3ce2704..4f1da285 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -451,17 +451,19 @@ def authorize(self, callback=None, state=None, **kwargs): state (e.g. CSRF tokens). :param kwargs: add optional key/value pairs to the query string """ + params = dict(self.request_token_params) or {} + params.update(**kwargs) + if self.request_token_url: token = self.generate_request_token(callback)[0] url = '%s?oauth_token=%s' % ( self.expand_url(http://github.com/self.authorize_url), url_quote(token) ) + if params: + url += '&' + url_encode(params) else: assert callback is not None, 'Callback is required OAuth2' - params = dict(self.request_token_params) or {} - params.update(**kwargs) - client = self.make_client() if 'scope' in params: From ecff7d5db4439b5c269303f3e4df28649f6a9088 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 2 Apr 2014 15:27:25 +0800 Subject: [PATCH 035/279] raise Exception when there is no token. https://github.com/lepture/flask-oauthlib/issues/71 --- flask_oauthlib/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 4f1da285..97a2fc0c 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -550,6 +550,11 @@ def handle_oauth1_response(self): client = self.make_client() client.verifier = request.args.get('oauth_verifier') tup = session.get('%s_oauthtok' % self.name) + if not tup: + raise OAuthException( + 'Token not found, maybe you disabled cookie', + type='token_not_found' + ) client.resource_owner_key = tup[0] client.resource_owner_secret = tup[1] From 39759fddc14e1dacd574be0cfbf7aa8805344054 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 2 Apr 2014 15:32:22 +0800 Subject: [PATCH 036/279] Example changes for facebook. Related issue: https://github.com/lepture/flask-oauthlib/issues/90 --- example/facebook.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/example/facebook.py b/example/facebook.py index 15928c3e..7c6da7ac 100644 --- a/example/facebook.py +++ b/example/facebook.py @@ -1,5 +1,5 @@ from flask import Flask, redirect, url_for, session, request -from flask_oauthlib.client import OAuth +from flask_oauthlib.client import OAuth, OAuthException FACEBOOK_APP_ID = '188477911223606' @@ -30,9 +30,12 @@ def index(): @app.route('/login') def login(): - return facebook.authorize(callback=url_for('facebook_authorized', + callback = url_for( + 'facebook_authorized', next=request.args.get('next') or request.referrer or None, - _external=True)) + _external=True + ) + return facebook.authorize(callback=callback) @app.route('/login/authorized') @@ -43,6 +46,9 @@ def facebook_authorized(resp): request.args['error_reason'], request.args['error_description'] ) + if isinstance(resp, OAuthException): + return 'Access denied: %s' % resp.message + session['oauth_token'] = (resp['access_token'], '') me = facebook.get('/me') return 'Logged in as id=%s name=%s redirect=%s' % \ From e65d3e6503943853dce1cb7cd72e918adb59c0e3 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 4 Apr 2014 00:04:57 +0800 Subject: [PATCH 037/279] Verify client secret. This is a secure bug. https://github.com/lepture/flask-oauthlib/issues/92 --- flask_oauthlib/provider/oauth2.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 796063ff..179611fb 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -516,6 +516,10 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): log.debug('Authenticate failed, client not found.') return False + if client.client_secret != request.client_secret: + log.debug('Authenticate client failed, secret not match.') + return False + # attach client on request for convenience request.client = client return True From 2d711e9152b795f50d9c2ef69a33bede1f6810ed Mon Sep 17 00:00:00 2001 From: Stian Prestholdt Date: Thu, 3 Apr 2014 08:30:35 +0200 Subject: [PATCH 038/279] Support client authentication for authorization-code grant type When using a confidential client it should be possible to obtain a token through authorization code grant type with a valid code. This makes `OAuth2RequestValidator.validate_code()` and `OAuth2RequestValidator.confirm_redirect_uri)` support retrieving client from either request.client or request.client_id. --- flask_oauthlib/provider/oauth2.py | 30 +++++++++++++++++++++++++----- tests/oauth2/server.py | 7 +++++++ tests/oauth2/test_oauth2.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 179611fb..a28b40c8 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -464,7 +464,24 @@ def __init__(self, clientgetter, tokengetter, grantgetter, self._grantsetter = grantsetter def client_authentication_required(self, request, *args, **kwargs): - return request.grant_type in ('password', 'refresh_token') + """Determine if client authentication is required for current request. + + According to the rfc6749, client authentication is required in the + following cases: + + Resource Owner Password Credentials Grant: see `Section 4.3.2`_. + Authorization Code Grant: see `Section 4.1.3`_. + Refresh Token Grant: see `Section 6`_. + + .. _`Section 4.3.2`: http://tools.ietf.org/html/rfc6749#section-4.3.2 + .. _`Section 4.1.3`: http://tools.ietf.org/html/rfc6749#section-4.1.3 + .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 + """ + if request.grant_type == 'password': + return True + auth_required = ('authorization_code', 'refresh_token') + return 'Authorization' in request.headers and\ + request.grant_type in auth_required def authenticate_client(self, request, *args, **kwargs): """Authenticate itself in other means. @@ -494,6 +511,7 @@ def authenticate_client(self, request, *args, **kwargs): return False request.client = client + if client.client_secret != client_secret: log.debug('Authenticate client failed, secret not match.') return False @@ -533,9 +551,10 @@ def confirm_redirect_uri(self, client_id, code, redirect_uri, client, add a `validate_redirect_uri` function on grant for a customized validation. """ + client = client or self._clientgetter(client_id) log.debug('Confirm redirect uri for client %r and code %r.', - client_id, code) - grant = self._grantgetter(client_id=client_id, code=code) + client.client_id, code) + grant = self._grantgetter(client_id=client.client_id, code=code) if not grant: log.debug('Grant not found.') return False @@ -672,10 +691,11 @@ def validate_client_id(self, client_id, request, *args, **kwargs): def validate_code(self, client_id, code, client, request, *args, **kwargs): """Ensure the grant code is valid.""" + client = client or self._clientgetter(client_id) log.debug( - 'Validate code for client %r and code %r', client_id, code + 'Validate code for client %r and code %r', client.client_id, code ) - grant = self._grantgetter(client_id=client_id, code=code) + grant = self._grantgetter(client_id=client.client_id, code=code) if not grant: log.debug('Grant not found.') return False diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index ef5155af..97348eab 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -217,10 +217,17 @@ def prepare_app(app): user = User(username='admin') + temp_grant = Grant( + user_id=1, client_id='confidential', + code='12345', scope='email', + expires=datetime.utcnow() + timedelta(seconds=100) + ) + try: db.session.add(client1) db.session.add(client2) db.session.add(user) + db.session.add(temp_grant) db.session.commit() except: db.session.rollback() diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index e4449a89..2ffadb19 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -297,3 +297,33 @@ def test_get_access_token(self): data = json.loads(u(rv.data)) assert data['access_token'] == 'foobar' assert data['refresh_token'] == 'foobar' + + +class TestConfidentialClient(OAuthSuite): + + def create_oauth_provider(self, app): + return default_provider(app) + + def test_get_access_token(self): + url = ('/oauth/token?grant_type=authorization_code&code=12345' + '&scope=email') + rv = self.client.get(url, headers={ + 'Authorization': 'Basic %s' % auth_code + }, data={'confirm': 'yes'}) + assert b'access_token' in rv.data + + def test_invalid_grant(self): + url = ('/oauth/token?grant_type=authorization_code&code=54321' + '&scope=email') + rv = self.client.get(url, headers={ + 'Authorization': 'Basic %s' % auth_code + }, data={'confirm': 'yes'}) + assert b'invalid_grant' in rv.data + + def test_invalid_client(self): + url = ('/oauth/token?grant_type=authorization_code&code=12345' + '&scope=email') + rv = self.client.get(url, headers={ + 'Authorization': 'Basic %s' % ('foo') + }, data={'confirm': 'yes'}) + assert b'invalid_client' in rv.data From 792d2768a2d461a5713736609abd41356407284d Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 30 Apr 2014 18:18:35 +0800 Subject: [PATCH 039/279] Python support for 3.4 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 0d4b19d0..d033f2a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - "2.6" - "2.7" - "3.3" + - "3.4" - "pypy" script: From 3af32f6a44f4ef7c37406fbd55a30f25ca7d968c Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 30 Apr 2014 18:19:10 +0800 Subject: [PATCH 040/279] Version to 0.5.0 --- docs/changelog.rst | 5 +++++ flask_oauthlib/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 29acb32f..e387b2e4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,11 @@ Changelog Here you can see the full list of changes between each Flask-OAuthlib release. +Version 0.5.0 +------------- + +Release date to be decided. + Version 0.4.3 ------------- diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index 2675ad10..e30f3ba4 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -11,7 +11,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = "0.4.3" +__version__ = "0.5.0" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' From 117bc2e3e8663f4f7ee25554b40f59770837b884 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 1 May 2014 00:21:01 +0800 Subject: [PATCH 041/279] require_oauth decorator should return 401 for invalid access token. https://github.com/lepture/flask-oauthlib/issues/93 --- flask_oauthlib/provider/oauth1.py | 2 +- flask_oauthlib/provider/oauth2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index 23a8b437..120b161e 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -504,7 +504,7 @@ def decorated(*args, **kwargs): valid, req = func(valid, req) if not valid: - return abort(403) + return abort(401) # alias user for convenience req.user = req.access_token.user request.oauth = req diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 9946395a..62dc2521 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -442,7 +442,7 @@ def decorated(*args, **kwargs): valid, req = func(valid, req) if not valid: - return abort(403) + return abort(401) request.oauth = req return f(*args, **kwargs) return decorated From 11e5ed04dcd6719ee9d102e4369e086f79905ed3 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 1 May 2014 00:32:16 +0800 Subject: [PATCH 042/279] Fix test cases. We have changed invalid access token response to 401. https://github.com/lepture/flask-oauthlib/issues/93 --- tests/oauth1/test_oauth1.py | 2 +- tests/oauth2/test_oauth2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/oauth1/test_oauth1.py b/tests/oauth1/test_oauth1.py index 51a57ce2..2c9d0723 100644 --- a/tests/oauth1/test_oauth1.py +++ b/tests/oauth1/test_oauth1.py @@ -60,7 +60,7 @@ def test_full_flow(self): assert 'email' in u(rv.data) rv = self.client.get('/address') - assert rv.status_code == 403 + assert rv.status_code == 401 rv = self.client.get('/method/post') assert 'POST' in u(rv.data) diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index 2ffadb19..ff8a92b9 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -106,7 +106,7 @@ def test_full_flow(self): assert b'username' in rv.data rv = self.client.get('/address') - assert rv.status_code == 403 + assert rv.status_code == 401 rv = self.client.get('/method/post') assert b'POST' in rv.data From 0c6c6775b69da4148d48906649b039180ceffdf4 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 11 May 2014 01:45:48 +0800 Subject: [PATCH 043/279] add apps factory and test it. --- flask_oauthlib/contrib/apps.py | 71 +++++++++++++++++++++++++++++++++ tests/test_contrib/__init__.py | 0 tests/test_contrib/test_apps.py | 39 ++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 flask_oauthlib/contrib/apps.py create mode 100644 tests/test_contrib/__init__.py create mode 100644 tests/test_contrib/test_apps.py diff --git a/flask_oauthlib/contrib/apps.py b/flask_oauthlib/contrib/apps.py new file mode 100644 index 00000000..c7c5f8f5 --- /dev/null +++ b/flask_oauthlib/contrib/apps.py @@ -0,0 +1,71 @@ +class RemoteAppFactory(object): + """The factory to create remote app and bind it to given extension. + + :param default_name: the default name which be used for registering. + :param kwargs: the pre-defined kwargs. + :param docstring: the docstring of factory. + """ + + def __init__(self, default_name, kwargs, docstring=''): + assert 'name' not in kwargs + assert 'register' not in kwargs + self.default_name = default_name + self.kwargs = kwargs + self._kwargs_processor = None + self.__doc__ = docstring + + def register_to(self, oauth, name=None, **kwargs): + """Creates a remote app and registers it.""" + kwargs = self._process_kwargs( + name=(name or self.default_name), **kwargs) + return oauth.remote_app(**kwargs) + + def create(self, oauth, **kwargs): + """Creates a remote app only.""" + kwargs = self._process_kwargs( + name=self.default_name, register=False, **kwargs) + return oauth.remote_app(**kwargs) + + def kwargs_processor(self, fn): + """Sets a function to process kwargs before creating any app.""" + self._kwargs_processor = fn + if fn.__doc__: + # appends docstring + self.__doc__ = '%s\n%s' % (self.__doc__, fn.__doc__) + return fn + + def _process_kwargs(self, **kwargs): + final_kwargs = dict(self.kwargs) + # merges with pre-defined kwargs + final_kwargs.update(kwargs) + # use name as app key + final_kwargs.setdefault('app_key', final_kwargs['name'].upper()) + # processes by pre-defined function + if self._kwargs_processor is not None: + final_kwargs = self._kwargs_processor(**final_kwargs) + return final_kwargs + + +douban = RemoteAppFactory('douban', { + 'base_url': 'https://api.douban.com/v2/', + 'access_token_url': 'https://www.douban.com/service/auth2/token', + 'authorize_url': 'https://www.douban.com/service/auth2/auth', + 'access_token_method': 'POST', +}, """The OAuth app for douban.com API.""") + + +@douban.kwargs_processor +def douban_kwargs_processor(**kwargs): + """ + :param scope: optional. default: ['douban_basic_common']. + see also: http://developers.douban.com/wiki/?title=oauth2 + """ + # request_token_url + kwargs.setdefault('request_token_url', None) + # request_token_params + scope = kwargs.pop('scope', ['douban_basic_common']) # default scope + if not isinstance(scope, basestring): + scope = ','.join(scope) # allows list-style scope + request_token_params = kwargs.setdefault('request_token_params', {}) + request_token_params.setdefault('scope', scope) # doesn't override exists + return kwargs diff --git a/tests/test_contrib/__init__.py b/tests/test_contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_contrib/test_apps.py b/tests/test_contrib/test_apps.py new file mode 100644 index 00000000..32ff367c --- /dev/null +++ b/tests/test_contrib/test_apps.py @@ -0,0 +1,39 @@ +import unittest + +from flask import Flask +from flask_oauthlib.client import OAuth +from flask_oauthlib.contrib.apps import douban +from nose.tools import assert_raises + + +class RemoteAppFactorySuite(unittest.TestCase): + + def setUp(self): + self.app = Flask(__name__) + self.oauth = OAuth(self.app) + + def test_douban(self): + assert 'douban.com' in douban.__doc__ + assert ':param scope:' in douban.__doc__ + + c1 = douban.create(self.oauth) + assert 'api.douban.com/v2' in c1.base_url + assert c1.request_token_params.get('scope') == 'douban_basic_common' + + with assert_raises(KeyError): + c1.consumer_key + with assert_raises(KeyError): + c1.consumer_secret + + self.app.config['DOUBAN_CONSUMER_KEY'] = 'douban key' + self.app.config['DOUBAN_CONSUMER_SECRET'] = 'douban secret' + assert c1.consumer_key == 'douban key' + assert c1.consumer_secret == 'douban secret' + + c2 = douban.register_to(self.oauth, 'doudou', scope=['a', 'b']) + assert c2.request_token_params.get('scope') == 'a,b' + + with assert_raises(KeyError): + c2.consumer_key + self.app.config['DOUDOU_CONSUMER_KEY'] = 'douban2 key' + assert c2.consumer_key == 'douban2 key' From 6bb4810eddedf5aa26f8c3e20115591b3f4a434d Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 11 May 2014 01:57:05 +0800 Subject: [PATCH 044/279] refine the docstring processing and add facebook, dropbox, github. --- flask_oauthlib/contrib/apps.py | 77 +++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/flask_oauthlib/contrib/apps.py b/flask_oauthlib/contrib/apps.py index c7c5f8f5..132d9e4c 100644 --- a/flask_oauthlib/contrib/apps.py +++ b/flask_oauthlib/contrib/apps.py @@ -12,7 +12,7 @@ def __init__(self, default_name, kwargs, docstring=''): self.default_name = default_name self.kwargs = kwargs self._kwargs_processor = None - self.__doc__ = docstring + self.__doc__ = docstring.lstrip() def register_to(self, oauth, name=None, **kwargs): """Creates a remote app and registers it.""" @@ -29,9 +29,6 @@ def create(self, oauth, **kwargs): def kwargs_processor(self, fn): """Sets a function to process kwargs before creating any app.""" self._kwargs_processor = fn - if fn.__doc__: - # appends docstring - self.__doc__ = '%s\n%s' % (self.__doc__, fn.__doc__) return fn def _process_kwargs(self, **kwargs): @@ -46,26 +43,66 @@ def _process_kwargs(self, **kwargs): return final_kwargs +def make_scope_processor(default_scope): + def processor(**kwargs): + # request_token_params + scope = kwargs.pop('scope', [default_scope]) # default scope + if not isinstance(scope, basestring): + scope = ','.join(scope) # allows list-style scope + request_token_params = kwargs.setdefault('request_token_params', {}) + request_token_params.setdefault('scope', scope) # doesn't override + return kwargs + return processor + + douban = RemoteAppFactory('douban', { 'base_url': 'https://api.douban.com/v2/', + 'request_token_url': None, 'access_token_url': 'https://www.douban.com/service/auth2/token', 'authorize_url': 'https://www.douban.com/service/auth2/auth', 'access_token_method': 'POST', -}, """The OAuth app for douban.com API.""") +}, """ +The OAuth app for douban.com API. +:param scope: optional. default: ['douban_basic_common']. + see also: http://developers.douban.com/wiki/?title=oauth2 +""") +douban.kwargs_processor(make_scope_processor('douban_basic_common')) -@douban.kwargs_processor -def douban_kwargs_processor(**kwargs): - """ - :param scope: optional. default: ['douban_basic_common']. - see also: http://developers.douban.com/wiki/?title=oauth2 - """ - # request_token_url - kwargs.setdefault('request_token_url', None) - # request_token_params - scope = kwargs.pop('scope', ['douban_basic_common']) # default scope - if not isinstance(scope, basestring): - scope = ','.join(scope) # allows list-style scope - request_token_params = kwargs.setdefault('request_token_params', {}) - request_token_params.setdefault('scope', scope) # doesn't override exists - return kwargs + +dropbox = RemoteAppFactory('dropbox', { + 'base_url': 'https://www.dropbox.com/1/', + 'request_token_url': None, + 'access_token_url': 'https://api.dropbox.com/1/oauth2/token', + 'authorize_url': 'https://www.dropbox.com/1/oauth2/authorize', + 'access_token_method': 'POST', + 'request_token_params': {}, +}, """The OAuth app for Dropbox API.""") + + +facebook = RemoteAppFactory('facebook', { + 'request_token_params': {'scope': 'email'}, + 'base_url': 'https://graph.facebook.com/', + 'request_token_url': None, + 'access_token_url': '/oauth/access_token', + 'authorize_url': 'https://www.facebook.com/dialog/oauth', +}, """ +The OAuth app for Facebook API. + +:param scope: optional. default: ['email']. +""") +facebook.kwargs_processor(make_scope_processor('email')) + + +github = RemoteAppFactory('github', { + 'base_url': 'https://api.github.com/', + 'request_token_url': None, + 'access_token_method': 'POST', + 'access_token_url': 'https://github.com/login/oauth/access_token', + 'authorize_url': 'https://github.com/login/oauth/authorize', +}, """ +The OAuth app for GitHub API. + +:param scope: optional. default: ['user:email']. +""") +github.kwargs_processor(make_scope_processor('user:email')) From ee0d6ca57dfba395c5df5a21c52be943d13dd026 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 11 May 2014 02:11:06 +0800 Subject: [PATCH 045/279] add more apps from the example source. --- flask_oauthlib/contrib/apps.py | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/flask_oauthlib/contrib/apps.py b/flask_oauthlib/contrib/apps.py index 132d9e4c..b7a9144b 100644 --- a/flask_oauthlib/contrib/apps.py +++ b/flask_oauthlib/contrib/apps.py @@ -106,3 +106,58 @@ def processor(**kwargs): :param scope: optional. default: ['user:email']. """) github.kwargs_processor(make_scope_processor('user:email')) + + +google = RemoteAppFactory('google', { + 'base_url': 'https://www.googleapis.com/oauth2/v1/', + 'request_token_url': None, + 'access_token_method': 'POST', + 'access_token_url': 'https://accounts.google.com/o/oauth2/token', + 'authorize_url': 'https://accounts.google.com/o/oauth2/auth', +}, """ +The OAuth app for Google API. + +:param scope: optional. + default: ['https://www.googleapis.com/auth/userinfo.email']. +""") +google.kwargs_processor(make_scope_processor( + 'https://www.googleapis.com/auth/userinfo.email')) + + +twitter = RemoteAppFactory('twitter', { + 'base_url': 'https://api.twitter.com/1.1/', + 'request_token_url': 'https://api.twitter.com/oauth/request_token', + 'access_token_url': 'https://api.twitter.com/oauth/access_token', + 'authorize_url': 'https://api.twitter.com/oauth/authenticate', +}, """The OAuth app for Twitter API.""") + + +weibo = RemoteAppFactory('weibo', { + 'base_url': 'https://api.weibo.com/2/', + 'authorize_url': 'https://api.weibo.com/oauth2/authorize', + 'request_token_url': None, + 'access_token_method': 'POST', + 'access_token_url': 'https://api.weibo.com/oauth2/access_token', + # since weibo's response is a shit, we need to force parse the content + 'content_type': 'application/json', +}, """ +The OAuth app for weibo.com API. + +:param scope: optional. default: ['email'] +""") +weibo.kwargs_processor(make_scope_processor('email')) + + +linkedin = RemoteAppFactory('linkedin', { + 'request_token_params': {'state': 'RandomString'}, + 'base_url': 'https://api.linkedin.com/v1/', + 'request_token_url': None, + 'access_token_method': 'POST', + 'access_token_url': 'https://www.linkedin.com/uas/oauth2/accessToken', + 'authorize_url': 'https://www.linkedin.com/uas/oauth2/authorization', +}, """ +The OAuth app for LinkedIn API. + +:param scope: optional. default: ['r_basicprofile'] +""") +linkedin.kwargs_processor(make_scope_processor('r_basicprofile')) From 9a11e4d449ff039070fe9ed548293a36f7df3df0 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 11 May 2014 02:20:18 +0800 Subject: [PATCH 046/279] test linkedin and fix up the deep copy issue. --- flask_oauthlib/contrib/apps.py | 7 +++++-- tests/test_contrib/test_apps.py | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/flask_oauthlib/contrib/apps.py b/flask_oauthlib/contrib/apps.py index b7a9144b..a48a1348 100644 --- a/flask_oauthlib/contrib/apps.py +++ b/flask_oauthlib/contrib/apps.py @@ -1,3 +1,6 @@ +import copy + + class RemoteAppFactory(object): """The factory to create remote app and bind it to given extension. @@ -32,9 +35,9 @@ def kwargs_processor(self, fn): return fn def _process_kwargs(self, **kwargs): - final_kwargs = dict(self.kwargs) + final_kwargs = copy.deepcopy(self.kwargs) # merges with pre-defined kwargs - final_kwargs.update(kwargs) + final_kwargs.update(copy.deepcopy(kwargs)) # use name as app key final_kwargs.setdefault('app_key', final_kwargs['name'].upper()) # processes by pre-defined function diff --git a/tests/test_contrib/test_apps.py b/tests/test_contrib/test_apps.py index 32ff367c..60efe4c3 100644 --- a/tests/test_contrib/test_apps.py +++ b/tests/test_contrib/test_apps.py @@ -2,7 +2,7 @@ from flask import Flask from flask_oauthlib.client import OAuth -from flask_oauthlib.contrib.apps import douban +from flask_oauthlib.contrib.apps import douban, linkedin from nose.tools import assert_raises @@ -37,3 +37,18 @@ def test_douban(self): c2.consumer_key self.app.config['DOUDOU_CONSUMER_KEY'] = 'douban2 key' assert c2.consumer_key == 'douban2 key' + + def test_linkedin(self): + c1 = linkedin.create(self.oauth) + assert c1.name == 'linkedin' + assert c1.request_token_params == { + 'state': 'RandomString', + 'scope': 'r_basicprofile', + } + + c2 = linkedin.register_to(self.oauth, name='l2', scope=['c', 'd']) + assert c2.name == 'l2' + assert c2.request_token_params == { + 'state': 'RandomString', + 'scope': 'c,d', + }, c2.request_token_params From 2791e96e5b7e88fa4e140c725513893e7b896084 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 11 May 2014 02:42:51 +0800 Subject: [PATCH 047/279] refine docs. --- docs/api.rst | 12 ++++++++++ flask_oauthlib/contrib/apps.py | 42 +++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index d2b65927..63f8bde0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -56,3 +56,15 @@ Here are APIs provided by contributors. .. autofunction:: bind_sqlalchemy .. autofunction:: bind_cache_grant + + +.. automodule:: flask_oauthlib.contrib.apps + + .. autofunction:: douban + .. autofunction:: dropbox + .. autofunction:: facebook + .. autofunction:: github + .. autofunction:: google + .. autofunction:: linkedin + .. autofunction:: twitter + .. autofunction:: weibo diff --git a/flask_oauthlib/contrib/apps.py b/flask_oauthlib/contrib/apps.py index a48a1348..ea0acb57 100644 --- a/flask_oauthlib/contrib/apps.py +++ b/flask_oauthlib/contrib/apps.py @@ -1,6 +1,36 @@ +""" + flask_oauthlib.contrib.apps + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + The bundle of remote app factories for famous third platforms. + + Usage:: + + from flask import Flask + from flask_oauthlib.client import OAuth + from flask_oauthlib.contrib.apps import twitter + + app = Flask(__name__) + oauth = OAuth(app) + + twitter.register_to(oauth, scope=['myscope']) + twitter.register_to(oauth, name='twitter2') + + Of course, it requires consumer keys in your config:: + + TWITTER_CONSUMER_KEY = '' + TWITTER_CONSUMER_SECRET = '' + TWITTER2_CONSUMER_KEY = '' + TWITTER2_CONSUMER_SECRET = '' +""" + import copy +__all__ = ['douban', 'dropbox', 'facebook', 'github', 'google', 'linkedin', + 'twitter', 'weibo'] + + class RemoteAppFactory(object): """The factory to create remote app and bind it to given extension. @@ -67,7 +97,7 @@ def processor(**kwargs): }, """ The OAuth app for douban.com API. -:param scope: optional. default: ['douban_basic_common']. +:param scope: optional. default: ``['douban_basic_common']``. see also: http://developers.douban.com/wiki/?title=oauth2 """) douban.kwargs_processor(make_scope_processor('douban_basic_common')) @@ -92,7 +122,7 @@ def processor(**kwargs): }, """ The OAuth app for Facebook API. -:param scope: optional. default: ['email']. +:param scope: optional. default: ``['email']``. """) facebook.kwargs_processor(make_scope_processor('email')) @@ -106,7 +136,7 @@ def processor(**kwargs): }, """ The OAuth app for GitHub API. -:param scope: optional. default: ['user:email']. +:param scope: optional. default: ``['user:email']``. """) github.kwargs_processor(make_scope_processor('user:email')) @@ -121,7 +151,7 @@ def processor(**kwargs): The OAuth app for Google API. :param scope: optional. - default: ['https://www.googleapis.com/auth/userinfo.email']. + default: ``['https://www.googleapis.com/auth/userinfo.email']``. """) google.kwargs_processor(make_scope_processor( 'https://www.googleapis.com/auth/userinfo.email')) @@ -146,7 +176,7 @@ def processor(**kwargs): }, """ The OAuth app for weibo.com API. -:param scope: optional. default: ['email'] +:param scope: optional. default: ``['email']`` """) weibo.kwargs_processor(make_scope_processor('email')) @@ -161,6 +191,6 @@ def processor(**kwargs): }, """ The OAuth app for LinkedIn API. -:param scope: optional. default: ['r_basicprofile'] +:param scope: optional. default: ``['r_basicprofile']`` """) linkedin.kwargs_processor(make_scope_processor('r_basicprofile')) From dd5fe66d38449f35e46aa08ed7974ce74d148353 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 11 May 2014 03:01:47 +0800 Subject: [PATCH 048/279] fix up the python 3.x compatible issue. --- flask_oauthlib/contrib/apps.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/contrib/apps.py b/flask_oauthlib/contrib/apps.py index ea0acb57..248dbc15 100644 --- a/flask_oauthlib/contrib/apps.py +++ b/flask_oauthlib/contrib/apps.py @@ -26,6 +26,8 @@ import copy +from oauthlib.common import unicode_type, bytes_type + __all__ = ['douban', 'dropbox', 'facebook', 'github', 'google', 'linkedin', 'twitter', 'weibo'] @@ -80,7 +82,7 @@ def make_scope_processor(default_scope): def processor(**kwargs): # request_token_params scope = kwargs.pop('scope', [default_scope]) # default scope - if not isinstance(scope, basestring): + if not isinstance(scope, (unicode_type, bytes_type)): scope = ','.join(scope) # allows list-style scope request_token_params = kwargs.setdefault('request_token_params', {}) request_token_params.setdefault('scope', scope) # doesn't override From 1d56709de17da2ee7833b0f7d16592a957406901 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 11 May 2014 03:02:13 +0800 Subject: [PATCH 049/279] fix up the python 2.6 compatible issue. --- tests/test_contrib/test_apps.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/test_contrib/test_apps.py b/tests/test_contrib/test_apps.py index 60efe4c3..3e57597c 100644 --- a/tests/test_contrib/test_apps.py +++ b/tests/test_contrib/test_apps.py @@ -20,10 +20,8 @@ def test_douban(self): assert 'api.douban.com/v2' in c1.base_url assert c1.request_token_params.get('scope') == 'douban_basic_common' - with assert_raises(KeyError): - c1.consumer_key - with assert_raises(KeyError): - c1.consumer_secret + assert_raises(KeyError, lambda: c1.consumer_key) + assert_raises(KeyError, lambda: c1.consumer_secret) self.app.config['DOUBAN_CONSUMER_KEY'] = 'douban key' self.app.config['DOUBAN_CONSUMER_SECRET'] = 'douban secret' @@ -33,8 +31,7 @@ def test_douban(self): c2 = douban.register_to(self.oauth, 'doudou', scope=['a', 'b']) assert c2.request_token_params.get('scope') == 'a,b' - with assert_raises(KeyError): - c2.consumer_key + assert_raises(KeyError, lambda: c2.consumer_key) self.app.config['DOUDOU_CONSUMER_KEY'] = 'douban2 key' assert c2.consumer_key == 'douban2 key' From 307e87142fca20200e0c4f1011bbdd8d73c1e9ca Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 13 May 2014 21:22:26 +0800 Subject: [PATCH 050/279] Add contributor in code file: https://github.com/lepture/flask-oauthlib/pull/94 --- flask_oauthlib/contrib/apps.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flask_oauthlib/contrib/apps.py b/flask_oauthlib/contrib/apps.py index 248dbc15..4f41a0ce 100644 --- a/flask_oauthlib/contrib/apps.py +++ b/flask_oauthlib/contrib/apps.py @@ -22,6 +22,8 @@ TWITTER_CONSUMER_SECRET = '' TWITTER2_CONSUMER_KEY = '' TWITTER2_CONSUMER_SECRET = '' + + Contributed by: tonyseek """ import copy From e11faa2c51931bc7c506912f2cc72c52fd3be83a Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 13 May 2014 21:50:30 +0800 Subject: [PATCH 051/279] Add @stianpr in author list --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index c6c60c22..8b0469e6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,3 +3,4 @@ - Mackenzie Blake Thompson https://github.com/flippmoke - Ib Lundgren https://github.com/ib-lundgren - Jiangge Zhang https://github.com/tonyseek +- Stian Prestholdt https://github.com/stianpr From cfcf649b1d42477f6af2e3ed8c7a2390b5539bec Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 13 May 2014 21:55:38 +0800 Subject: [PATCH 052/279] Changelog for 0.5.0. Version bump to 0.5.0 --- docs/changelog.rst | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e387b2e4..a96e627d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,7 +8,50 @@ Here you can see the full list of changes between each Flask-OAuthlib release. Version 0.5.0 ------------- -Release date to be decided. +Released on May 13, 2014 + +- Add ``contrib.apps`` module, thanks for tonyseek via `#94`_. +- Status code changed to 401 for invalid access token via `#93`_. +- **Security bug** for access token via `#92`_. +- Fix for client part, request token params for OAuth1 via `#91`_. +- **API change** for ``oauth.require_oauth`` via `#89`_. +- Fix for OAuth2 provider, support client authentication for authorization-code grant type via `#86`_. +- Fix client_credentials logic in validate_grant_type via `#85`_. +- Fix for client part, pass access token method via `#83`_. +- Fix for OAuth2 provider related to confidential client via `#82`_. + +Upgrade From 0.4.x to 0.5.0 +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +API for OAuth providers ``oauth.require_oauth`` has changed. + +Before the change, you would write code like:: + + @app.route('/api/user') + @oauth.require_oauth('email'): + def user(req): + return jsonify(req.user) + +After the change, you would write code like:: + + from flask import request + + @app.route('/api/user') + @oauth.require_oauth('email'): + def user(): + return jsonify(request.oauth.user) + +.. _`#94`: https://github.com/lepture/flask-oauthlib/pull/94 +.. _`#93`: https://github.com/lepture/flask-oauthlib/issues/93 +.. _`#92`: https://github.com/lepture/flask-oauthlib/issues/92 +.. _`#91`: https://github.com/lepture/flask-oauthlib/issues/91 +.. _`#89`: https://github.com/lepture/flask-oauthlib/issues/89 +.. _`#86`: https://github.com/lepture/flask-oauthlib/pull/86 +.. _`#85`: https://github.com/lepture/flask-oauthlib/pull/85 +.. _`#83`: https://github.com/lepture/flask-oauthlib/pull/83 +.. _`#82`: https://github.com/lepture/flask-oauthlib/issues/82 + +Thanks Stian Prestholdt and Jiangge Zhang. Version 0.4.3 ------------- From e7d84f2e3328830fb14c43a10d8f3cfbd6b677e0 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 13 May 2014 22:08:53 +0800 Subject: [PATCH 053/279] Fix documentation --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a96e627d..8ff14494 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -28,7 +28,7 @@ API for OAuth providers ``oauth.require_oauth`` has changed. Before the change, you would write code like:: @app.route('/api/user') - @oauth.require_oauth('email'): + @oauth.require_oauth('email') def user(req): return jsonify(req.user) @@ -37,7 +37,7 @@ After the change, you would write code like:: from flask import request @app.route('/api/user') - @oauth.require_oauth('email'): + @oauth.require_oauth('email') def user(): return jsonify(request.oauth.user) From 7b7f45297080bf5b0790899c82338f59ef7a0d75 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 13 May 2014 22:14:56 +0800 Subject: [PATCH 054/279] Add Python 3.4 to classifiers --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a31cd3dd..ae45fcec 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ def fread(filename): 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', From f94475378f0ec766b0cc96492e62282fc4d92d6d Mon Sep 17 00:00:00 2001 From: Bartek Ciszkowski Date: Thu, 22 May 2014 13:25:21 -0400 Subject: [PATCH 055/279] Update references to db.relationship in oauth2 docs It is not apparent right away where `relationship` comes from. Ideally, we just prefix it with db like the rest of the model definition. --- docs/oauth2.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/oauth2.rst b/docs/oauth2.rst index cb0a249e..d09a9f89 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -68,7 +68,7 @@ An example of the data model in SQLAlchemy (SQLAlchemy is not required):: # creator of the client, not required user_id = db.Column(db.ForeignKey('user.id')) # required if you need to support client credential - user = relationship('User') + user = db.relationship('User') client_id = db.Column(db.String(40), primary_key=True) client_secret = db.Column(db.String(55), unique=True, index=True, @@ -128,13 +128,13 @@ Also in SQLAlchemy model (would be better if it is in a cache):: user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) - user = relationship('User') + user = db.relationship('User') client_id = db.Column( db.String(40), db.ForeignKey('client.client_id'), nullable=False, ) - client = relationship('Client') + client = db.relationship('Client') code = db.Column(db.String(255), index=True, nullable=False) @@ -178,12 +178,12 @@ An example of the data model in SQLAlchemy:: db.String(40), db.ForeignKey('client.client_id'), nullable=False, ) - client = relationship('Client') + client = db.relationship('Client') user_id = db.Column( db.Integer, db.ForeignKey('user.id') ) - user = relationship('User') + user = db.relationship('User') # currently only bearer is supported token_type = db.Column(db.String(40)) From a0434fad0f412d90822f357edae0b38937b826d7 Mon Sep 17 00:00:00 2001 From: Horace Thomas Date: Sat, 24 May 2014 15:43:04 -0400 Subject: [PATCH 056/279] converting header names to str. --- flask_oauthlib/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oauthlib/utils.py b/flask_oauthlib/utils.py index 1b8f0aee..23cc14d7 100644 --- a/flask_oauthlib/utils.py +++ b/flask_oauthlib/utils.py @@ -38,7 +38,7 @@ def create_response(headers, body, status): """Create response class for Flask.""" response = Response(body or '') for k, v in headers.items(): - response.headers[k] = v + response.headers[str(k)] = v response.status_code = status return response From f8e110d8f86fc38ad33e0e3592d3edd5ab811986 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 4 Jun 2014 15:10:33 +0800 Subject: [PATCH 057/279] Update docs for 0.5.0 --- docs/oauth1.rst | 20 ++++++-------------- docs/oauth2.rst | 21 ++++++--------------- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/docs/oauth1.rst b/docs/oauth1.rst index 5a6e7eb7..69fba61a 100644 --- a/docs/oauth1.rst +++ b/docs/oauth1.rst @@ -442,21 +442,22 @@ Protect the resource of a user with ``require_oauth`` decorator now:: @app.route('/api/me') @oauth.require_oauth('email') - def me(request): - user = request.user + def me(): + user = request.oauth.user return jsonify(email=user.email, username=user.username) @app.route('/api/user/') @oauth.require_oauth('email') - def user(request, username): + def user(username): user = User.query.filter_by(username=username).first() return jsonify(email=user.email, username=user.username) The decorator accepts a list of realms, only the clients with the given realms can access the defined resources. -The handlers accepts an extended parameter ``request``, as we have explained -above, it contains at least: +.. versionchanged:: 0.5.0 + +The ``request`` has an additional property ``oauth``, it contains at least: - client: client model object - realms: a list of scopes @@ -464,15 +465,6 @@ above, it contains at least: - headers: headers of the request - body: body content of the request -You may find the name confused, since Flask has a ``request`` model, you can -rename it to other names, for exmaple:: - - @app.route('/api/me') - @oauth.require_oauth('email', 'username') - def me(data): - user = data.user - return jsonify(email=user.email, username=user.username) - Example for OAuth 1 ------------------- diff --git a/docs/oauth2.rst b/docs/oauth2.rst index d09a9f89..b5870ae0 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -443,21 +443,22 @@ Protect the resource of a user with ``require_oauth`` decorator now:: @app.route('/api/me') @oauth.require_oauth('email') - def me(request): - user = request.user + def me(): + user = request.oauth.user return jsonify(email=user.email, username=user.username) @app.route('/api/user/') @oauth.require_oauth('email') - def user(request, username): + def user(username): user = User.query.filter_by(username=username).first() return jsonify(email=user.email, username=user.username) The decorator accepts a list of scopes, only the clients with the given scopes can access the defined resources. -The handlers accepts an extended parameter ``request``, as we have explained -above, it contains at least: +.. versionchanged:: 0.5.0 + +The ``request`` has an additional property ``oauth``, it contains at least: - client: client model object - scopes: a list of scopes @@ -468,16 +469,6 @@ above, it contains at least: - state: state parameter - response_type: response_type paramter -You may find the name confused, since Flask has a ``request`` model, you can -rename it to other names, for exmaple:: - - @app.route('/api/me') - @oauth.require_oauth('email', 'username') - def me(data): - user = data.user - return jsonify(email=user.email, username=user.username) - - Example for OAuth 2 ------------------- From 076fa554c1bd5c033b416b9d9f556b92a72610b9 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 5 Jun 2014 17:02:58 +0800 Subject: [PATCH 058/279] Add pre request for contrib.apps --- flask_oauthlib/contrib/apps.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/flask_oauthlib/contrib/apps.py b/flask_oauthlib/contrib/apps.py index 4f41a0ce..5a2531a7 100644 --- a/flask_oauthlib/contrib/apps.py +++ b/flask_oauthlib/contrib/apps.py @@ -185,6 +185,18 @@ def processor(**kwargs): weibo.kwargs_processor(make_scope_processor('email')) +def change_weibo_header(uri, headers, body): + """Since weibo is a rubbish server, it does not follow the standard, + we need to change the authorization header for it.""" + auth = headers.get('Authorization') + if auth: + auth = auth.replace('Bearer', 'OAuth2') + headers['Authorization'] = auth + return uri, headers, body + +weibo.pre_request = change_weibo_header + + linkedin = RemoteAppFactory('linkedin', { 'request_token_params': {'state': 'RandomString'}, 'base_url': 'https://api.linkedin.com/v1/', @@ -198,3 +210,17 @@ def processor(**kwargs): :param scope: optional. default: ``['r_basicprofile']`` """) linkedin.kwargs_processor(make_scope_processor('r_basicprofile')) + + +def change_linkedin_query(uri, headers, body): + auth = headers.pop('Authorization') + headers['x-li-format'] = 'json' + if auth: + auth = auth.replace('Bearer', '').strip() + if '?' in uri: + uri += '&oauth2_access_token=' + auth + else: + uri += '?oauth2_access_token=' + auth + return uri, headers, body + +linkedin.pre_request = change_linkedin_query From a8895d1b9b634258397265869aa439028595ad2d Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sun, 15 Jun 2014 20:27:55 -0300 Subject: [PATCH 059/279] Fix some oauth2 doc typos --- docs/oauth2.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/oauth2.rst b/docs/oauth2.rst index b5870ae0..09714a66 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -3,7 +3,7 @@ OAuth2 Server ============= -An OAuth2 server concerns how to grant the auothorization and how to protect +An OAuth2 server concerns how to grant the authorization and how to protect the resource. Register an **OAuth** provider:: from flask_oauthlib.provider import OAuth2Provider @@ -20,12 +20,12 @@ Like any other Flask extensions, we can pass the application later:: oauth.init_app(app) return app -To implemente the oauthorization flow, we need to understand the data model. +To implement the authorization flow, we need to understand the data model. User (Resource Owner) --------------------- -A user, or resource owner, is usally the registered user on your site. You +A user, or resource owner, is usually the registered user on your site. You design your own user model, there is not much to say. Client (Application) From 14ab0ed54a6c8331ebeabc12373d0c43237c3256 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 16 Jun 2014 16:59:27 +0800 Subject: [PATCH 060/279] Incompatible with 0.6.2 and 0.6.3 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index da38f09d..ceef37e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ Flask Mock -oauthlib +oauthlib==0.6.1 Flask-SQLAlchemy diff --git a/setup.py b/setup.py index ae45fcec..df8c54a0 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def fread(filename): license='BSD', install_requires=[ 'Flask', - 'oauthlib>=0.6', + 'oauthlib==0.6.1', ], tests_require=['nose', 'Flask-SQLAlchemy', 'mock'], test_suite='nose.collector', From cc6e1a9d43645d4e0da62140ce7e32a09678fadd Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 3 Jul 2014 00:10:07 +0800 Subject: [PATCH 061/279] Update douban example for #102 --- example/douban.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example/douban.py b/example/douban.py index 70865afc..2ad4a21b 100644 --- a/example/douban.py +++ b/example/douban.py @@ -11,9 +11,9 @@ 'douban', consumer_key='0cfc3c5d9f873b1826f4b518de95b148', consumer_secret='3e209e4f9ecf6a4a', - base_url='https://api.douban.com/v2/', + base_url='https://api.douban.com/', request_token_url=None, - request_token_params={'scope': 'douban_basic_common'}, + request_token_params={'scope': 'douban_basic_common,shuo_basic_r'}, access_token_url='https://www.douban.com/service/auth2/token', authorize_url='https://www.douban.com/service/auth2/auth', access_token_method='POST', @@ -23,8 +23,8 @@ @app.route('/') def index(): if 'douban_token' in session: - resp = douban.get('user/~me') - return jsonify(resp.data) + resp = douban.get('shuo/v2/statuses/home_timeline') + return jsonify(status=resp.status, data=resp.data) return redirect(url_for('login')) From fcdd7f6f20ff2dc17ba3eb763b44b60520443ad8 Mon Sep 17 00:00:00 2001 From: Adel Qalieh Date: Tue, 8 Jul 2014 12:24:42 -0400 Subject: [PATCH 062/279] Tiny typo on OAuth2 server documentation rediret_uri -> redirect_uri --- docs/oauth2.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/oauth2.rst b/docs/oauth2.rst index 09714a66..50dd7edf 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -275,7 +275,7 @@ information: - client: client model object - scopes: a list of scopes - user: user model object -- redirect_uri: rediret_uri parameter +- redirect_uri: redirect_uri parameter - headers: headers of the request - body: body content of the request - state: state parameter @@ -463,7 +463,7 @@ The ``request`` has an additional property ``oauth``, it contains at least: - client: client model object - scopes: a list of scopes - user: user model object -- redirect_uri: rediret_uri parameter +- redirect_uri: redirect_uri parameter - headers: headers of the request - body: body content of the request - state: state parameter From 59beb45755536f8f9d0b73394d5cee534ee2b1e1 Mon Sep 17 00:00:00 2001 From: Gary Belvin Date: Fri, 11 Jul 2014 07:08:46 -0700 Subject: [PATCH 063/279] Cleaned up documentation. --- flask_oauthlib/client.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 97a2fc0c..fa8c3710 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -398,7 +398,7 @@ def request(self, url, data=None, headers=None, format='urlencoded', is provided, the data is passed as it, and the `format` is ignored. :param token: an optional token to pass, if it is None, token will - be generated be tokengetter. + be generated by tokengetter. """ headers = dict(headers or {}) @@ -430,9 +430,9 @@ def request(self, url, data=None, headers=None, format='urlencoded', ) if hasattr(self, 'pre_request'): - # this is desgined for some rubbish service like weibo - # since they don't follow the standards, we need to - # change the uri, headers, or body + # This is desgined for some rubbish service like weibo. + # Since they don't follow the standards, we need to + # change the uri, headers, or body. uri, headers, body = self.pre_request(uri, headers, body) resp, content = self.http_request( @@ -620,6 +620,7 @@ def handle_unknown_response(self): return None def authorized_handler(self, f): + """Handles an OAuth callback.""" @wraps(f) def decorated(*args, **kwargs): if 'oauth_verifier' in request.args: From cae003cb7c4cef74335599428e6e00228faff4c4 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 18 Jul 2014 15:38:05 +0800 Subject: [PATCH 064/279] Compatible with OAuthLib 0.6.2 and 0.6.3. https://github.com/lepture/flask-oauthlib/issues/100 --- flask_oauthlib/provider/oauth2.py | 3 ++- setup.py | 2 +- tests/_base.py | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 62dc2521..b150b87f 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -567,7 +567,8 @@ def confirm_redirect_uri(self, client_id, code, redirect_uri, client, log.debug('Compare redirect uri for grant %r and %r.', grant.redirect_uri, redirect_uri) - if os.environ.get('DEBUG') and redirect_uri is None: + testing = 'OAUTHLIB_INSECURE_TRANSPORT' in os.environ + if testing and redirect_uri is None: # For testing return True diff --git a/setup.py b/setup.py index df8c54a0..f9abd284 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def fread(filename): license='BSD', install_requires=[ 'Flask', - 'oauthlib==0.6.1', + 'oauthlib=>0.6.2', ], tests_require=['nose', 'Flask-SQLAlchemy', 'mock'], test_suite='nose.collector', diff --git a/tests/_base.py b/tests/_base.py index 69e28196..4359d485 100644 --- a/tests/_base.py +++ b/tests/_base.py @@ -17,7 +17,9 @@ python_version = 2 string_type = unicode -os.environ['DEBUG'] = 'true' +# os.environ['DEBUG'] = 'true' +# for oauthlib 0.6.3 +os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = 'true' class BaseSuite(unittest.TestCase): From fb9f638275f7cc1eb8bd5b7311a04a043379a9ab Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 18 Jul 2014 16:11:08 +0800 Subject: [PATCH 065/279] Add error message for validate_bear_token on oauth2. https://github.com/lepture/flask-oauthlib/issues/110 --- flask_oauthlib/provider/oauth2.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index b150b87f..73adf89e 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -661,17 +661,23 @@ def validate_bearer_token(self, token, scopes, request): log.debug('Validate bearer token %r', token) tok = self._tokengetter(access_token=token) if not tok: - log.debug('Bearer token not found.') + msg = 'Bearer token not found.' + request.error_message = msg + log.debug(msg) return False # validate expires if datetime.datetime.utcnow() > tok.expires: - log.debug('Bearer token is expired.') + msg = 'Bearer token is expired.' + request.error_message = msg + log.debug(msg) return False # validate scopes if not set(tok.scopes).issuperset(set(scopes)): - log.debug('Bearer token scope not valid.') + msg = 'Bearer token scope not valid.' + request.error_message = msg + log.debug(msg) return False request.access_token = tok From 930e7468e805a24c50035658af64b754e6a96a37 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 18 Jul 2014 16:31:08 +0800 Subject: [PATCH 066/279] Add `invalid_response` decorator for custom error response on `require_oauth`. https://github.com/lepture/flask-oauthlib/issues/104 https://github.com/lepture/flask-oauthlib/issues/110 --- flask_oauthlib/provider/oauth2.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 73adf89e..07fb3d49 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -72,6 +72,7 @@ def user(): def __init__(self, app=None): self._before_request_funcs = [] self._after_request_funcs = [] + self._invalid_response = None if app: self.init_app(app) @@ -199,6 +200,22 @@ def valid_after_request(valid, oauth): self._after_request_funcs.append(f) return f + def invalid_response(self, f): + """Register a function for responsing with invalid request. + + When an invalid request proceeds to :meth:`require_oauth`, we can + handle the request with the registered function. The function + accepts one parameter, which is an oauthlib Request object:: + + @oauth.invalid_response + def invalid_require_oauth(req): + return jsonify(message=req.error_message), 401 + + If no function is registered, it will return with ``abort(401)``. + """ + self._invalid_response = f + return f + def clientgetter(self, f): """Register a function as the client getter. @@ -442,6 +459,8 @@ def decorated(*args, **kwargs): valid, req = func(valid, req) if not valid: + if self._invalid_response: + return self._invalid_response(req) return abort(401) request.oauth = req return f(*args, **kwargs) From 0b3b04419dacae719f5bc40ea6e438ae3a9ef438 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 18 Jul 2014 16:53:49 +0800 Subject: [PATCH 067/279] Fix requirements for testing --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ceef37e5..8e740079 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ Flask Mock -oauthlib==0.6.1 +oauthlib=>0.6.2 Flask-SQLAlchemy From 015641050a8fb4d46bb97145eefe17363774c2bd Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 18 Jul 2014 17:13:53 +0800 Subject: [PATCH 068/279] Fix test cases for oauth2. Add test case for `invalid_response` decorator. https://github.com/lepture/flask-oauthlib/issues/110 --- tests/oauth2/client.py | 6 +++--- tests/oauth2/server.py | 9 ++++++++- tests/oauth2/test_oauth2.py | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/oauth2/client.py b/tests/oauth2/client.py index ae62b2c0..9affa60f 100644 --- a/tests/oauth2/client.py +++ b/tests/oauth2/client.py @@ -48,7 +48,7 @@ def authorized(resp): @app.route('/client') def client_method(): ret = remote.get("client") - if ret.status not in (200,201): + if ret.status not in (200, 201): return abort(ret.status) return ret.raw_data @@ -56,7 +56,7 @@ def client_method(): def address(): ret = remote.get('address/hangzhou') if ret.status not in (200, 201): - return abort(ret.status) + return ret.raw_data, ret.status return ret.raw_data @app.route('/method/') @@ -74,7 +74,7 @@ def get_oauth_token(): if __name__ == '__main__': import os - os.environ['DEBUG'] = 'true' + os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = 'true' # DEBUG=1 python oauth2_client.py app = Flask(__name__) app.debug = True diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index 3da5a87d..6a1a44be 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -234,7 +234,10 @@ def prepare_app(app): return app -def create_server(app, oauth): +def create_server(app, oauth=None): + if not oauth: + oauth = default_provider(app) + app = prepare_app(app) @app.before_request @@ -285,6 +288,10 @@ def address_api(city): def method_api(): return jsonify(method=request.method) + @oauth.invalid_response + def require_oauth_invalid(req): + return jsonify(message=req.error_message), 401 + return app diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index ff8a92b9..44d58587 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -107,6 +107,7 @@ def test_full_flow(self): rv = self.client.get('/address') assert rv.status_code == 401 + assert b'message' in rv.data rv = self.client.get('/method/post') assert b'POST' in rv.data From 6bcdc9b2fc31bd6a6b4eef55da989ba8e032069a Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 18 Jul 2014 17:30:39 +0800 Subject: [PATCH 069/279] Add test case for no bear token and expired bear token for oauth2 --- tests/oauth2/server.py | 5 +++++ tests/oauth2/test_oauth2.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index 6a1a44be..0c23b090 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -223,11 +223,16 @@ def prepare_app(app): expires=datetime.utcnow() + timedelta(seconds=100) ) + access_token = Token( + user_id=1, client_id='dev', access_token='expired', expires_in=0 + ) + try: db.session.add(client1) db.session.add(client2) db.session.add(user) db.session.add(temp_grant) + db.session.add(access_token) db.session.commit() except: db.session.rollback() diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index 44d58587..53af83e6 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -40,6 +40,7 @@ def setup_app(self, app): client.http_request = MagicMock( side_effect=self.patch_request(app) ) + self.oauth_client = client return app @@ -118,6 +119,22 @@ def test_full_flow(self): rv = self.client.get('/method/delete') assert b'DELETE' in rv.data + def test_no_bear_token(self): + @self.oauth_client.tokengetter + def get_oauth_token(): + return 'foo', '' + + rv = self.client.get('/method/put') + assert b'token not found' in rv.data + + def test_expires_bear_token(self): + @self.oauth_client.tokengetter + def get_oauth_token(): + return 'expired', '' + + rv = self.client.get('/method/put') + assert b'token is expired' in rv.data + def test_get_client(self): rv = self.client.post(authorize_url, data={'confirm': 'yes'}) rv = self.client.get(clean_url(http://github.com/rv.location)) From fb648a8355eeb36eee10ae2a056c3e4e1cde79da Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 18 Jul 2014 17:32:56 +0800 Subject: [PATCH 070/279] requirements with no version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8e740079..da38f09d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ Flask Mock -oauthlib=>0.6.2 +oauthlib Flask-SQLAlchemy From 32cf2e7767d25d2820cf27f62be0e8b987ad82bf Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 18 Jul 2014 18:19:01 +0800 Subject: [PATCH 071/279] Add test case for refresh token in authorization code flow. https://github.com/lepture/flask-oauthlib/issues/81 --- tests/oauth2/test_oauth2.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index 53af83e6..d268d982 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -234,6 +234,20 @@ def test_refresh_token_in_password_grant(self): }) assert b'access_token' in rv.data + def test_refresh_token_in_authorization_code(self): + rv = self.client.post(authorize_url, data={'confirm': 'yes'}) + rv = self.client.get(clean_url(http://github.com/rv.location)) + data = json.loads(u(rv.data)) + + args = (data.get('scope').replace(' ', '+'), + data.get('refresh_token'), 'dev', 'dev') + url = ('/oauth/token?grant_type=refresh_token' + '&scope=%s&refresh_token=%s' + '&client_id=%s&client_secret=%s') + url = url % args + rv = self.client.get(url) + assert b'access_token' in rv.data + class TestRefreshTokenCached(TestRefreshToken): From 65bdbd1edf51d6ede63b1edaf9c82e306ec41914 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 18 Jul 2014 18:21:26 +0800 Subject: [PATCH 072/279] Fix test case in refresh token in password grant. Remove the confusion of username=admin https://github.com/lepture/flask-oauthlib/issues/101 --- tests/oauth2/test_oauth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index d268d982..d140a480 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -227,7 +227,7 @@ def test_refresh_token_in_password_grant(self): args = (data.get('scope').replace(' ', '+'), data.get('refresh_token')) url = ('/oauth/token?grant_type=refresh_token' - '&scope=%s&refresh_token=%s&username=admin') + '&scope=%s&refresh_token=%s') url = url % args rv = self.client.get(url, headers={ 'Authorization': 'Basic %s' % auth_code, From f5dbe27d0c01d9b073671ba601465f80b94f8a2d Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 18 Jul 2014 18:29:07 +0800 Subject: [PATCH 073/279] Remove usename and password in client_credential flow. --- tests/oauth2/test_oauth2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index d140a480..40090719 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -268,7 +268,7 @@ def create_oauth_provider(self, app): def test_get_access_token(self): url = ('/oauth/token?grant_type=client_credentials' - '&scope=email+address&username=admin&password=admin') + '&scope=email+address') rv = self.client.get(url, headers={ 'Authorization': 'Basic %s' % auth_code, }, data={'confirm': 'yes'}) @@ -276,7 +276,7 @@ def test_get_access_token(self): def test_invalid_auth_header(self): url = ('/oauth/token?grant_type=client_credentials' - '&scope=email+address&username=admin&password=admin') + '&scope=email+address') rv = self.client.get(url, headers={ 'Authorization': 'Basic foobar' }, data={'confirm': 'yes'}) @@ -285,7 +285,7 @@ def test_invalid_auth_header(self): def test_no_client(self): auth_code = _base64('none:confidential') url = ('/oauth/token?grant_type=client_credentials' - '&scope=email+address&username=admin&password=admin') + '&scope=email+address') rv = self.client.get(url, headers={ 'Authorization': 'Basic %s' % auth_code, }, data={'confirm': 'yes'}) @@ -294,7 +294,7 @@ def test_no_client(self): def test_wrong_secret_client(self): auth_code = _base64('confidential:wrong') url = ('/oauth/token?grant_type=client_credentials' - '&scope=email+address&username=admin&password=admin') + '&scope=email+address') rv = self.client.get(url, headers={ 'Authorization': 'Basic %s' % auth_code, }, data={'confirm': 'yes'}) From 216b676a730bdf7113cd6b8a4646d13ae4abd05e Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Mon, 21 Jul 2014 20:48:20 +0800 Subject: [PATCH 074/279] Correct the misleading example of app factories. Close #111 --- flask_oauthlib/contrib/apps.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/flask_oauthlib/contrib/apps.py b/flask_oauthlib/contrib/apps.py index 5a2531a7..ecb3dece 100644 --- a/flask_oauthlib/contrib/apps.py +++ b/flask_oauthlib/contrib/apps.py @@ -8,20 +8,23 @@ from flask import Flask from flask_oauthlib.client import OAuth - from flask_oauthlib.contrib.apps import twitter + from flask_oauthlib.contrib.apps import github app = Flask(__name__) oauth = OAuth(app) - twitter.register_to(oauth, scope=['myscope']) - twitter.register_to(oauth, name='twitter2') + github.register_to(oauth, scope=['user:email']) + github.register_to(oauth, name='github2') Of course, it requires consumer keys in your config:: - TWITTER_CONSUMER_KEY = '' - TWITTER_CONSUMER_SECRET = '' - TWITTER2_CONSUMER_KEY = '' - TWITTER2_CONSUMER_SECRET = '' + GITHUB_CONSUMER_KEY = '' + GITHUB_CONSUMER_SECRET = '' + GITHUB2_CONSUMER_KEY = '' + GITHUB2_CONSUMER_SECRET = '' + + Some apps with OAuth 1.0a such as Twitter could not accept the ``scope`` + argument. Contributed by: tonyseek """ From e5a9cb7d7d78e9252b8c7c74706cfaaa3caa654d Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 21 Jul 2014 22:14:13 +0800 Subject: [PATCH 075/279] Fix test cases, remove useless data in case for misunderstanding --- tests/oauth2/test_oauth2.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index 40090719..4c0de953 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -184,7 +184,7 @@ def test_get_access_token(self): '&scope=email+address&username=admin&password=admin') rv = self.client.get(url, headers={ 'Authorization': 'Basic %s' % auth_code, - }, data={'confirm': 'yes'}) + }) assert b'access_token' in rv.data assert b'state' in rv.data @@ -193,7 +193,7 @@ def test_invalid_user_credentials(self): '&scope=email+address&username=fake&password=admin') rv = self.client.get(url, headers={ 'Authorization': 'Basic %s' % auth_code, - }, data={'confirm': 'yes'}) + }) assert b'Invalid credentials given' in rv.data @@ -271,7 +271,7 @@ def test_get_access_token(self): '&scope=email+address') rv = self.client.get(url, headers={ 'Authorization': 'Basic %s' % auth_code, - }, data={'confirm': 'yes'}) + }) assert b'access_token' in rv.data def test_invalid_auth_header(self): @@ -279,7 +279,7 @@ def test_invalid_auth_header(self): '&scope=email+address') rv = self.client.get(url, headers={ 'Authorization': 'Basic foobar' - }, data={'confirm': 'yes'}) + }) assert b'invalid_client' in rv.data def test_no_client(self): @@ -288,7 +288,7 @@ def test_no_client(self): '&scope=email+address') rv = self.client.get(url, headers={ 'Authorization': 'Basic %s' % auth_code, - }, data={'confirm': 'yes'}) + }) assert b'invalid_client' in rv.data def test_wrong_secret_client(self): @@ -297,7 +297,7 @@ def test_wrong_secret_client(self): '&scope=email+address') rv = self.client.get(url, headers={ 'Authorization': 'Basic %s' % auth_code, - }, data={'confirm': 'yes'}) + }) assert b'invalid_client' in rv.data @@ -341,7 +341,7 @@ def test_get_access_token(self): '&scope=email') rv = self.client.get(url, headers={ 'Authorization': 'Basic %s' % auth_code - }, data={'confirm': 'yes'}) + }) assert b'access_token' in rv.data def test_invalid_grant(self): @@ -349,7 +349,7 @@ def test_invalid_grant(self): '&scope=email') rv = self.client.get(url, headers={ 'Authorization': 'Basic %s' % auth_code - }, data={'confirm': 'yes'}) + }) assert b'invalid_grant' in rv.data def test_invalid_client(self): @@ -357,5 +357,5 @@ def test_invalid_client(self): '&scope=email') rv = self.client.get(url, headers={ 'Authorization': 'Basic %s' % ('foo') - }, data={'confirm': 'yes'}) + }) assert b'invalid_client' in rv.data From ce567498223407576c5850b97658524122475ef5 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 22 Jul 2014 14:32:17 +0800 Subject: [PATCH 076/279] Method for handle response smartly for client. --- flask_oauthlib/client.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index fa8c3710..c50e9e99 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -619,26 +619,31 @@ def handle_unknown_response(self): """Handles a unknown authorization response.""" return None + def handle_response(self): + """Handles authorization response smartly.""" + if 'oauth_verifier' in request.args: + try: + data = self.handle_oauth1_response() + except OAuthException as e: + data = e + elif 'code' in request.args: + try: + data = self.handle_oauth2_response() + except OAuthException as e: + data = e + else: + data = self.handle_unknown_response() + + # free request token + session.pop('%s_oauthtok' % self.name, None) + session.pop('%s_oauthredir' % self.name, None) + return data + def authorized_handler(self, f): """Handles an OAuth callback.""" @wraps(f) def decorated(*args, **kwargs): - if 'oauth_verifier' in request.args: - try: - data = self.handle_oauth1_response() - except OAuthException as e: - data = e - elif 'code' in request.args: - try: - data = self.handle_oauth2_response() - except OAuthException as e: - data = e - else: - data = self.handle_unknown_response() - - # free request token - session.pop('%s_oauthtok' % self.name, None) - session.pop('%s_oauthredir' % self.name, None) + data = self.handle_response() return f(*((data,) + args), **kwargs) return decorated From 5ad1fb73a7c2305c8b57cd8472015b87a8b3e171 Mon Sep 17 00:00:00 2001 From: Christofer Bertonha Date: Tue, 22 Jul 2014 20:15:13 -0300 Subject: [PATCH 077/279] Fix install_requires in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f9abd284..c2e476e4 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def fread(filename): license='BSD', install_requires=[ 'Flask', - 'oauthlib=>0.6.2', + 'oauthlib>=0.6.2', ], tests_require=['nose', 'Flask-SQLAlchemy', 'mock'], test_suite='nose.collector', From c563ce16dfa9d986110f10e0583f901a551e6bc2 Mon Sep 17 00:00:00 2001 From: Christofer Bertonha Date: Tue, 22 Jul 2014 20:31:58 -0300 Subject: [PATCH 078/279] Allow authorize_handler to allow get and post thead response --- flask_oauthlib/provider/oauth2.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 07fb3d49..23531f4a 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -372,18 +372,28 @@ def decorated(*args, **kwargs): scopes, credentials = ret kwargs['scopes'] = scopes kwargs.update(credentials) - return f(*args, **kwargs) except oauth2.FatalClientError as e: log.debug('Fatal client error %r', e) return redirect(e.in_uri(self.error_uri)) - if request.method == 'POST': + else: redirect_uri = request.values.get('redirect_uri', None) - if not f(*args, **kwargs): - # denied by user - e = oauth2.AccessDeniedError() - return redirect(e.in_uri(redirect_uri)) - return self.confirm_authorization_request() + + try: + rv = f(*args, **kwargs) + except oauth2.FatalClientError as e: + log.debug('Fatal client error %r', e) + return redirect(e.in_uri(self.error_uri)) + + if not isinstance(rv, bool): + # if is a response or redirect + return rv + + if not rv: + # denied by user + e = oauth2.AccessDeniedError() + return redirect(e.in_uri(redirect_uri)) + return self.confirm_authorization_request() return decorated def confirm_authorization_request(self): From ec5425ff632d3ed46714545828885e9043b8b7d6 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Thu, 24 Jul 2014 10:30:53 -0300 Subject: [PATCH 079/279] Copied docs from Flask-OAuth about oauth1 client --- docs/client.rst | 199 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/docs/client.rst b/docs/client.rst index 3e64447c..997e8a3b 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -16,6 +16,205 @@ The difference between OAuth1 and OAuth2 in the configuation is ``request_token_url``. In OAuth1 it is required, in OAuth2 it should be ``None``. +To connect to a remote application create a :class:`OAuth` +object and register a remote application on it using +the :meth:`~OAuth.remote_app` method:: + + from flask_oauthlib.client import OAuth + + oauth = OAuth() + the_remote_app = oauth.remote_app('the remote app', + ... + ) + +A remote application must define several URLs required by the +OAuth machinery: + +- `request_token_url` +- `access_token_url` +- `authorize_url` + +Additionally the application should define an issued `consumer_key` +and `consumer_secret`. + +You can find these values by registering your application with the remote +application you want to connect with. + +Additionally you can provide a `base_url` that is prefixed to *all* +relative URLs used in the remote app. + +For Twitter the setup would look like this:: + + twitter = oauth.remote_app('twitter', + base_url='https://api.twitter.com/1/', + request_token_url='https://api.twitter.com/oauth/request_token', + access_token_url='https://api.twitter.com/oauth/access_token', + authorize_url='https://api.twitter.com/oauth/authenticate', + consumer_key='', + consumer_secret='' + ) + +Now that the application is created one can start using the OAuth system. +One thing is missing: the tokengetter. OAuth uses a token and a secret to +figure out who is connecting to the remote application. After +authentication/authorization this information is passed to a function on +your side and it is your responsibility to remember it. + +The following rules apply: + +- It's your responsibility to store that information somewhere +- That information lives for as long as the user did not revoke the + access for your application on the remote application. If it was + revoked and the user re-enabled the application you will get different + keys, so if you store them in the database don't forget to check if + they changed in the authorization callback. +- During the authorization handshake a temporary token and secret are + issued. Your tokengetter is not used during that period. + +For a simple test application, storing that information in the session is +probably sufficient:: + + from flask import session + + @twitter.tokengetter + def get_twitter_token(token=None): + return session.get('twitter_token') + +If the token does not exist, the function must return `None`, and +otherwise return a tuple in the form ``(token, secret)``. The function +might also be passed a `token` parameter. This is user defined and can be +used to indicate another token. Imagine for instance you want to support +user and application tokens or different tokens for the same user. + +The name of the token can be passed to to the +:meth:`~OAuthRemoteApp.request` function. + +Signing in / Authorizing +------------------------ + +To sign in with Twitter or link a user account with a remote +Twitter user, simply call into +:meth:`~OAuthRemoteApp.authorize` and pass it the URL that the user should be +redirected back to. For example:: + + @app.route('/login') + def login(): + return twitter.authorize(callback=url_for('oauth_authorized', + next=request.args.get('next') or request.referrer or None)) + +If the application redirects back, the remote application will have passed all +relevant information to the `oauth_authorized` function: a special +response object with all the data, or ``None`` if the user denied the +request. This function must be decorated as +:meth:`~OAuthRemoteApp.authorized_handler`:: + + from flask import redirect + + @app.route('/oauth-authorized') + @twitter.authorized_handler + def oauth_authorized(resp): + next_url = request.args.get('next') or url_for('index') + if resp is None: + flash(u'You denied the request to sign in.') + return redirect(next_url) + + session['twitter_token'] = ( + resp['oauth_token'], + resp['oauth_token_secret'] + ) + session['twitter_user'] = resp['screen_name'] + + flash('You were signed in as %s' % resp['screen_name']) + return redirect(next_url) + +We store the token and the associated secret in the session so that the +tokengetter can return it. Additionally we also store the Twitter username +that was sent back to us so that we can later display it to the user. In +larger applications it is recommended to store satellite information in a +database instead to ease debugging and more easily handle additional information +associated with the user. + +Facebook OAuth +-------------- + +For Facebook the flow is very similar to Twitter or other OAuth systems +but there is a small difference. You're not using the `request_token_url` +at all and you need to provide a scope in the `request_token_params`:: + + facebook = oauth.remote_app('facebook', + base_url='https://graph.facebook.com/', + request_token_url=None, + access_token_url='/oauth/access_token', + authorize_url='https://www.facebook.com/dialog/oauth', + consumer_key=FACEBOOK_APP_ID, + consumer_secret=FACEBOOK_APP_SECRET, + request_token_params={'scope': 'email'} + ) + +Furthermore the `callback` is mandatory for the call to +:meth:`~OAuthRemoteApp.authorize` and has to match the base URL that was +specified in the Facebook application control panel. For development you +can set it to ``localhost:5000``. + +The `APP_ID` and `APP_SECRET` can be retrieved from the Facebook app +control panel. If you don't have an application registered yet you can do +this at `facebook.com/developers `_. + +Invoking Remote Methods +----------------------- + +Now the user is signed in, but you probably want to use +OAuth to call protected remote API methods and not just sign in. For +that, the remote application object provides a +:meth:`~OAuthRemoteApp.request` method that can request information from +an OAuth protected resource. Additionally there are shortcuts like +:meth:`~OAuthRemoteApp.get` or :meth:`~OAuthRemoteApp.post` to request +data with a certain HTTP method. + +For example to create a new tweet you would call into the Twitter +application as follows:: + + resp = twitter.post('statuses/update.json', data={ + 'status': 'The text we want to tweet' + }) + if resp.status == 403: + flash('Your tweet was too long.') + else: + flash('Successfully tweeted your tweet (ID: #%s)' % resp.data['id']) + +Or to display the users' feed we can do something like this:: + + resp = twitter.get('statuses/home_timeline.json') + if resp.status == 200: + tweets = resp.data + else: + tweets = None + flash('Unable to load tweets from Twitter. Maybe out of ' + 'API calls or Twitter is overloaded.') + +Flask-OAuthlib will do its best to send data encoded in the right format to +the server and to decode it when it comes back. Incoming data is encoded +based on the `mimetype` the server sent and is stored in the +:attr:`~OAuthResponse.data` attribute. For outgoing data a default of +``'urlencode'`` is assumed. When a different format is needed, one can +specify it with the `format` parameter. The following formats are +supported: + +**Outgoing**: + - ``'urlencode'`` - form encoded data (`GET` as URL and `POST`/`PUT` as + request body) + - ``'json'`` - JSON encoded data (`POST`/`PUT` as request body) + +**Incoming** + - ``'urlencode'`` - stored as flat unicode dictionary + - ``'json'`` - decoded with JSON rules, most likely a dictionary + - ``'xml'`` - stored as elementtree element + +Unknown incoming data is stored as a string. If outgoing data of a different +format is needed, `content_type` should be specified instead and the +data provided should be an encoded string. + + Find the OAuth1 client example at `twitter.py`_. .. _`twitter.py`: https://github.com/lepture/flask-oauthlib/blob/master/example/twitter.py From 99587f6e4cc1ecce86419510bde3d7d4167d1f0f Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Wed, 23 Jul 2014 14:03:32 -0300 Subject: [PATCH 080/279] Added tox for python version 2.6, 2.7, 3.2, 3.3 and 3.4 and pypy --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..9e35d5d3 --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist = py26,py27,py32,py33,py34,pypy + +[testenv] +commands = python setup.py test From 4acb956652001f79d041a8799134868a76ccb648 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Wed, 23 Jul 2014 14:03:59 -0300 Subject: [PATCH 081/279] Added .egg directory to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 223235a0..36e9635c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ docs/_build cover/ venv/ .tox +*.egg From 8fb549c42e231dfb48601507ffa258b0a41a9d1f Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 29 Jul 2014 15:03:49 +0800 Subject: [PATCH 082/279] You can request with a blank token. https://github.com/lepture/flask-oauthlib/pull/121 --- flask_oauthlib/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index c50e9e99..60d7c027 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -402,7 +402,7 @@ def request(self, url, data=None, headers=None, format='urlencoded', """ headers = dict(headers or {}) - if not token: + if token is None: token = self.get_request_token() client = self.make_client(token) From d80cc3fcaa023c14440f988750dd3748e36375ae Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Fri, 25 Jul 2014 09:38:19 -0300 Subject: [PATCH 083/279] Added note about session overriding on client docs --- docs/client.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/client.rst b/docs/client.rst index 997e8a3b..4186109c 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -6,6 +6,12 @@ the imports:: from flask_oauthlib.client import OAuth +.. attention:: If you are testing the provider and the client locally, do not + start they listening on the same address because they will + override the `session` of each other leading to strange bugs. + eg: start the provider listening on `127.0.0.1:4000` and client + listening on `localhost:4000` to avoid this problem. + .. _`Flask-OAuth`: http://pythonhosted.org/Flask-OAuth/ From 0c30023f18cd7c89d1c40ebd3547e45084f61078 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 29 Jul 2014 15:20:27 +0800 Subject: [PATCH 084/279] Version bump 0.6.0 --- docs/changelog.rst | 9 +++++++++ flask_oauthlib/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8ff14494..fbec2243 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,15 @@ Changelog Here you can see the full list of changes between each Flask-OAuthlib release. +Version 0.6.0 +------------- + +Released on Jul 29, 2014 + +- Compatible with OAuthLib 0.6.2 and 0.6.3 +- Add invalid_response decorator to handle invalid request +- Add error_message for OAuthLib Request. + Version 0.5.0 ------------- diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index e30f3ba4..206f3812 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -11,7 +11,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = "0.5.0" +__version__ = "0.6.0" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' From 4b1e7594ce6f5534bb106d5e1c55895130c0503a Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sat, 2 Aug 2014 22:34:28 +0800 Subject: [PATCH 085/279] Improve the example for usergetter. https://github.com/lepture/flask-oauthlib/issues/122 --- flask_oauthlib/provider/oauth2.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 23531f4a..f890c8cd 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -249,13 +249,22 @@ def get_client(client_id): def usergetter(self, f): """Register a function as the user getter. - This decorator is only required for password credential + This decorator is only required for **password credential** authorization:: @oauth.usergetter - def get_user(username=username, password=password, + def get_user(username, password, client, request, *args, **kwargs): - return get_user_by_username(username, password) + # client: current request client + if not client.has_password_credential_permission: + return None + user = User.get_user_by_username(username) + if not user.validate_password(password): + return None + + # parameter `request` is an OAuthlib Request object. + # maybe you will need it somewhere + return user """ self._usergetter = f return f From 14116ead5534f42744deb356c6aa876891c6b0b8 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 4 Aug 2014 19:09:43 +0800 Subject: [PATCH 086/279] Fix this damn mock bug in Python 3.4.1 --- Makefile | 1 + tests/test_client.py | 10 ++++++++++ tox.ini | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a8a5a793..da9fef36 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ clean: clean-build clean-pyc clean-docs clean-build: @rm -fr build/ @rm -fr dist/ + @rm -fr *.egg @rm -fr *.egg-info diff --git a/tests/test_client.py b/tests/test_client.py index 6dc021e1..03c80d8c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -160,6 +160,16 @@ def test_raise_http_request(self, urlopen): 'http://example.com/', 404, 'Not Found', None, None ) error.read = lambda: b'o' + + class _Fake(object): + def close(self): + return 0 + + class _Faker(object): + _closer = _Fake() + + error.file = _Faker() + urlopen.side_effect = error resp, content = OAuthRemoteApp.http_request('http://example.com/') assert resp.code == 404 diff --git a/tox.ini b/tox.ini index 9e35d5d3..6276c453 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py32,py33,py34,pypy +envlist = py26,py27,py33,py34,pypy [testenv] commands = python setup.py test From b5ef04277c38890a977721c656973d40d825aa88 Mon Sep 17 00:00:00 2001 From: Arnav Kumar Date: Mon, 4 Aug 2014 18:35:02 +0800 Subject: [PATCH 087/279] Fixing a small grammatical error in client.rst --- docs/client.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/client.rst b/docs/client.rst index 4186109c..8328f9da 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -7,7 +7,7 @@ the imports:: from flask_oauthlib.client import OAuth .. attention:: If you are testing the provider and the client locally, do not - start they listening on the same address because they will + make them start listening on the same address because they will override the `session` of each other leading to strange bugs. eg: start the provider listening on `127.0.0.1:4000` and client listening on `localhost:4000` to avoid this problem. From 88c028cecbdc88ea301e687c3e7d123a527243e6 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 5 Aug 2014 16:06:56 +0800 Subject: [PATCH 088/279] Fix for tox --- tox.ini | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6276c453..e1b24487 100644 --- a/tox.ini +++ b/tox.ini @@ -2,4 +2,8 @@ envlist = py26,py27,py33,py34,pypy [testenv] -commands = python setup.py test +deps = + nose + Mock + Flask-SQLAlchemy +commands = nosetests -s From 8dd88fd92fa80b10483d9d974342c7542d9c663b Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 8 Aug 2014 12:18:08 +0800 Subject: [PATCH 089/279] Handle all exceptions in oauth2 provider. https://github.com/lepture/flask-oauthlib/issues/125 --- flask_oauthlib/provider/oauth2.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index f890c8cd..2c140945 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -18,7 +18,7 @@ from werkzeug.utils import import_string from oauthlib import oauth2 from oauthlib.oauth2 import RequestValidator, Server -from oauthlib.common import to_unicode +from oauthlib.common import to_unicode, add_params_to_uri from ..utils import extract_params, decode_base64, create_response __all__ = ('OAuth2Provider', 'OAuth2RequestValidator') @@ -384,6 +384,10 @@ def decorated(*args, **kwargs): except oauth2.FatalClientError as e: log.debug('Fatal client error %r', e) return redirect(e.in_uri(self.error_uri)) + except Exception as e: + return redirect(add_params_to_uri( + self.error_uri, {'error': e.message} + )) else: redirect_uri = request.values.get('redirect_uri', None) @@ -393,6 +397,10 @@ def decorated(*args, **kwargs): except oauth2.FatalClientError as e: log.debug('Fatal client error %r', e) return redirect(e.in_uri(self.error_uri)) + except Exception as e: + return redirect(add_params_to_uri( + self.error_uri, {'error': e.message} + )) if not isinstance(rv, bool): # if is a response or redirect @@ -430,6 +438,10 @@ def confirm_authorization_request(self): return redirect(e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: return redirect(e.in_uri(redirect_uri)) + except Exception as e: + return redirect(add_params_to_uri( + self.error_uri, {'error': e.message} + )) def token_handler(self, f): """Access/refresh token handler decorator. From 514ff3bf4db002e9eaeefa544d5a967c8b8a2fe4 Mon Sep 17 00:00:00 2001 From: Arnav Kumar Date: Tue, 5 Aug 2014 19:28:49 +0800 Subject: [PATCH 090/279] Update documentation and fix typos. --- docs/oauth2.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/oauth2.rst b/docs/oauth2.rst index 50dd7edf..09fa122a 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -34,7 +34,7 @@ Client (Application) A client is the app which want to use the resource of a user. It is suggested that the client is registered by a user on your site, but it is not required. -The client should contain at least these information: +The client should contain at least these properties: - client_id: A random string - client_secret: A random string @@ -216,15 +216,15 @@ config: ================================== ========================================== -Implements ----------- +Implementation +-------------- -The implementings of authorization flow needs two handlers, one is authorize -handler for user to confirm the grant, the other is token handler for client -to exchange/refresh access token. +The implementation of authorization flow needs two handlers, one is the authorization +handler for the user to confirm the grant, the other is the token handler for the client +to exchange/refresh access tokens. Before the implementing of authorize and token handler, we need to set up some -getters and setter to communicate with the database. +getters and setters to communicate with the database. Client getter ````````````` @@ -269,7 +269,7 @@ implemented with decorators:: In the sample code, there is a ``get_current_user`` method, that will return the current user object, you should implement it yourself. -The ``request`` object is defined by ``OAuthlib``, you can get at least these +The ``request`` object is defined by ``OAuthlib``, you can get at least this much information: - client: client model object @@ -284,8 +284,8 @@ information: Token getter and setter ``````````````````````` -Token getter and setters are required. They are used in the authorization flow -and accessing resource flow. Implemented with decorators:: +Token getter and setter are required. They are used in the authorization flow +and accessing resource flow. They are implemented with decorators as follows:: @oauth.tokengetter def load_token(access_token=None, refresh_token=None): @@ -378,8 +378,8 @@ kwargs are: - redirect_uri: redirect_uri parameter - response_type: response_type parameter -The POST request needs to return a bool value that tells whether user grantted -the access or not. +The POST request needs to return a bool value that tells whether user granted +access or not. There is a ``@require_login`` decorator in the sample code, you should implement it yourself. @@ -388,7 +388,7 @@ implement it yourself. Token handler ````````````` -Token handler is a decorator for exchange/refresh access token. You don't need +Token handler is a decorator for exchanging/refreshing access token. You don't need to do much:: @app.route('/oauth/token') @@ -425,7 +425,7 @@ Subclass way ```````````` If you are not satisfied with the decorator way of getters and setters, you can -implements them in the subclass way:: +implement them in the subclass way:: class MyProvider(OAuth2Provider): def _clientgetter(self, client_id): @@ -453,7 +453,7 @@ Protect the resource of a user with ``require_oauth`` decorator now:: user = User.query.filter_by(username=username).first() return jsonify(email=user.email, username=user.username) -The decorator accepts a list of scopes, only the clients with the given scopes +The decorator accepts a list of scopes and only the clients with the given scopes can access the defined resources. .. versionchanged:: 0.5.0 From 9918de4694b94a1ddec91bf15604a3f6b5e35d1b Mon Sep 17 00:00:00 2001 From: rmihael Date: Sun, 10 Aug 2014 16:39:51 +0300 Subject: [PATCH 091/279] Fix a typo in oauth2 provider. May be I'm missing something, but it seems like a typo --- flask_oauthlib/provider/oauth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 2c140945..6b35b8dc 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -815,7 +815,7 @@ def validate_redirect_uri(self, client_id, redirect_uri, request, redirect_uris strictly, you can add a `validate_redirect_uri` function on grant for a customized validation. """ - request.client = request.client = self._clientgetter(client_id) + request.client = request.client or self._clientgetter(client_id) client = request.client if hasattr(client, 'validate_redirect_uri'): return client.validate_redirect_uri(redirect_uri) From b7f895815c96d56a387d1fa511e11ae5ffe30115 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 13 Aug 2014 23:51:50 +0800 Subject: [PATCH 092/279] Handle exception in OAuth1 --- flask_oauthlib/provider/oauth1.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index 120b161e..02ed8fe9 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -408,6 +408,10 @@ def decorated(*args, **kwargs): return redirect(e.in_uri(self.error_uri)) except errors.InvalidClientError as e: return redirect(e.in_uri(self.error_uri)) + except Exception as e: + return redirect(add_params_to_uri( + self.error_uri, {'error': e.message} + )) return decorated def confirm_authorization_request(self): @@ -428,6 +432,10 @@ def confirm_authorization_request(self): return redirect(e.in_uri(self.error_uri)) except errors.InvalidClientError as e: return redirect(e.in_uri(self.error_uri)) + except Exception as e: + return redirect(add_params_to_uri( + self.error_uri, {'error': e.message} + )) def request_token_handler(self, f): """Request token handler decorator. @@ -454,6 +462,10 @@ def decorated(*args, **kwargs): return create_response(*ret) except errors.OAuth1Error as e: return _error_response(e) + except Exception as e: + e.urlencoded = 'error=%s' % e.message + e.status_code = 400 + return _error_response(e) return decorated def access_token_handler(self, f): @@ -481,6 +493,10 @@ def decorated(*args, **kwargs): return create_response(*ret) except errors.OAuth1Error as e: return _error_response(e) + except Exception as e: + e.urlencoded = 'error=%s' % e.message + e.status_code = 400 + return _error_response(e) return decorated def require_oauth(self, *realms, **kwargs): From 7dc883bb4d088264c2fa332e3372152c534b3ad5 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 13 Aug 2014 23:52:08 +0800 Subject: [PATCH 093/279] Stylefix for OAuth2 Provider --- flask_oauthlib/provider/oauth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 6b35b8dc..d931ab4d 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -535,7 +535,7 @@ def client_authentication_required(self, request, *args, **kwargs): return True auth_required = ('authorization_code', 'refresh_token') return 'Authorization' in request.headers and\ - request.grant_type in auth_required + request.grant_type in auth_required def authenticate_client(self, request, *args, **kwargs): """Authenticate itself in other means. From 68d305ba0d698c713a005a3204cd5eed3bc11311 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 14 Aug 2014 10:24:12 +0800 Subject: [PATCH 094/279] urlencode right with the help of oauthlib.common --- flask_oauthlib/provider/oauth1.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index 02ed8fe9..eb858333 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -16,7 +16,7 @@ from oauthlib.oauth1 import RequestValidator from oauthlib.oauth1 import WebApplicationServer as Server from oauthlib.oauth1 import SIGNATURE_HMAC, SIGNATURE_RSA -from oauthlib.common import to_unicode, add_params_to_uri +from oauthlib.common import to_unicode, add_params_to_uri, urlencode from oauthlib.oauth1.rfc5849 import errors from ..utils import extract_params, create_response @@ -463,7 +463,7 @@ def decorated(*args, **kwargs): except errors.OAuth1Error as e: return _error_response(e) except Exception as e: - e.urlencoded = 'error=%s' % e.message + e.urlencoded = urlencode([('error', str(e))]) e.status_code = 400 return _error_response(e) return decorated @@ -494,7 +494,7 @@ def decorated(*args, **kwargs): except errors.OAuth1Error as e: return _error_response(e) except Exception as e: - e.urlencoded = 'error=%s' % e.message + e.urlencoded = urlencode([('error', str(e))]) e.status_code = 400 return _error_response(e) return decorated From 7102484e423686e9bc31b32688531b5fa4e827e9 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 14 Aug 2014 10:24:24 +0800 Subject: [PATCH 095/279] Add test case for invalid urlencoded --- tests/oauth1/test_oauth1.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/oauth1/test_oauth1.py b/tests/oauth1/test_oauth1.py index 2c9d0723..d2ab0bf8 100644 --- a/tests/oauth1/test_oauth1.py +++ b/tests/oauth1/test_oauth1.py @@ -95,6 +95,13 @@ def test_invalid_request_token(self): }) assert 'error' in rv.location + def test_invalid_urlencoded(self): + rv = self.client.get('/oauth/request_token?query=tam%20q') + assert b'non+urlencoded' in rv.data + rv = self.client.get('/oauth/access_token?query=tam%20q') + assert b'non+urlencoded' in rv.data + + auth_header = ( u'OAuth realm="%(realm)s",' u'oauth_nonce="97392753692390970531372987366",' From 85da0e062598ea99ad059ed0eb328e3e9ba00b05 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 14 Aug 2014 10:39:19 +0800 Subject: [PATCH 096/279] Try for validate_protected_resource_request Fix https://github.com/lepture/flask-oauthlib/issues/128 --- flask_oauthlib/provider/oauth1.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index eb858333..8ed9964f 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -512,10 +512,14 @@ def decorated(*args, **kwargs): server = self.server uri, http_method, body, headers = extract_params() - valid, req = server.validate_protected_resource_request( - uri, http_method, body, headers, realms - ) - + try: + valid, req = server.validate_protected_resource_request( + uri, http_method, body, headers, realms + ) + except Exception as e: + e.urlencoded = urlencode([('error', str(e))]) + e.status_code = 400 + return _error_response(e) for func in self._after_request_funcs: valid, req = func(valid, req) From 9c2d2e96600a424344f61835f3404f7d1281b65c Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 18 Aug 2014 10:36:13 +0800 Subject: [PATCH 097/279] Change handle_response to authrized_response --- flask_oauthlib/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 60d7c027..4fc13fe2 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -619,7 +619,7 @@ def handle_unknown_response(self): """Handles a unknown authorization response.""" return None - def handle_response(self): + def authorized_response(self): """Handles authorization response smartly.""" if 'oauth_verifier' in request.args: try: @@ -643,7 +643,7 @@ def authorized_handler(self, f): """Handles an OAuth callback.""" @wraps(f) def decorated(*args, **kwargs): - data = self.handle_response() + data = self.authorized_response() return f(*((data,) + args), **kwargs) return decorated From 80a6082f4ee1e2def5ffafba1ec3bde521259c7c Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 18 Aug 2014 10:36:21 +0800 Subject: [PATCH 098/279] Prefer authorized_response instead of @authorized_handler in example --- example/douban.py | 4 ++-- example/dropbox.py | 4 ++-- example/facebook.py | 4 ++-- example/github.py | 4 ++-- example/google.py | 4 ++-- example/linkedin.py | 4 ++-- example/twitter.py | 4 ++-- example/weibo.py | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/example/douban.py b/example/douban.py index 2ad4a21b..257f9e3e 100644 --- a/example/douban.py +++ b/example/douban.py @@ -40,8 +40,8 @@ def logout(): @app.route('/login/authorized') -@douban.authorized_handler -def authorized(resp): +def authorized(): + resp = douban.authorized_response() if resp is None: return 'Access denied: reason=%s error=%s' % ( request.args['error_reason'], diff --git a/example/dropbox.py b/example/dropbox.py index 658710ed..6bbc8808 100644 --- a/example/dropbox.py +++ b/example/dropbox.py @@ -40,8 +40,8 @@ def logout(): @app.route('/login/authorized') -@dropbox.authorized_handler -def authorized(resp): +def authorized(): + resp = dropbox.authorized_response() if resp is None: return 'Access denied: reason=%s error=%s' % ( request.args['error_reason'], diff --git a/example/facebook.py b/example/facebook.py index 7c6da7ac..2d4473f8 100644 --- a/example/facebook.py +++ b/example/facebook.py @@ -39,8 +39,8 @@ def login(): @app.route('/login/authorized') -@facebook.authorized_handler -def facebook_authorized(resp): +def facebook_authorized(): + resp = facebook.authorized_response() if resp is None: return 'Access denied: reason=%s error=%s' % ( request.args['error_reason'], diff --git a/example/github.py b/example/github.py index f80b896e..119770b7 100644 --- a/example/github.py +++ b/example/github.py @@ -40,8 +40,8 @@ def logout(): @app.route('/login/authorized') -@github.authorized_handler -def authorized(resp): +def authorized(): + resp = github.authorized_response() if resp is None: return 'Access denied: reason=%s error=%s' % ( request.args['error_reason'], diff --git a/example/google.py b/example/google.py index ec43ae4c..60ad174a 100644 --- a/example/google.py +++ b/example/google.py @@ -52,8 +52,8 @@ def logout(): @app.route('/login/authorized') -@google.authorized_handler -def authorized(resp): +def authorized(): + resp = google.authorized_response() if resp is None: return 'Access denied: reason=%s error=%s' % ( request.args['error_reason'], diff --git a/example/linkedin.py b/example/linkedin.py index a9ad6c87..de898396 100644 --- a/example/linkedin.py +++ b/example/linkedin.py @@ -43,8 +43,8 @@ def logout(): @app.route('/login/authorized') -@linkedin.authorized_handler -def authorized(resp): +def authorized(): + resp = linkedin.authorized_response() if resp is None: return 'Access denied: reason=%s error=%s' % ( request.args['error_reason'], diff --git a/example/twitter.py b/example/twitter.py index 061e1343..f25ca785 100644 --- a/example/twitter.py +++ b/example/twitter.py @@ -81,8 +81,8 @@ def logout(): @app.route('/oauthorized') -@twitter.authorized_handler -def oauthorized(resp): +def oauthorized(): + resp = twitter.authorized_response() if resp is None: flash('You denied the request to sign in.') else: diff --git a/example/weibo.py b/example/weibo.py index 7f548ce8..a4c8404c 100644 --- a/example/weibo.py +++ b/example/weibo.py @@ -45,8 +45,8 @@ def logout(): @app.route('/login/authorized') -@weibo.authorized_handler -def authorized(resp): +def authorized(): + resp = weibo.authorized_response() if resp is None: return 'Access denied: reason=%s error=%s' % ( request.args['error_reason'], From e01265599904d5a0eabe1c1d6fd32a1467c16b8a Mon Sep 17 00:00:00 2001 From: Misja Hoebe Date: Sat, 16 Aug 2014 22:28:02 +0200 Subject: [PATCH 099/279] Implement token revocation endpoint Implements the OAuth2 [token revocation endpoint](https://oauthlib.readthedocs.org/en/latest/oauth2/endpoints/revocation.html). --- flask_oauthlib/provider/oauth2.py | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index d931ab4d..4d159c3e 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -469,6 +469,36 @@ def decorated(*args, **kwargs): return create_response(*ret) return decorated + def revoke_handler(self, f): + """Access/refresh token revoke decorator. + + Any return value by the decorated function will get discarded as defined + in [`RFC7009`_]. + + You can control the access method with the standard flask routing mechanism, + as per [`RFC7009`_] it is recommended to only allow the `POST` method:: + + @app.route('/oauth/revoke', methods=['POST']) + @oauth.revoke_handler + def access_token(): pass + + .. _`RFC7009`: http://tools.ietf.org/html/rfc7009 + """ + @wraps(f) + def decorated(*args, **kwargs): + server = self.server + + token = request.values.get('token') + request.token_type_hint = request.values.get('token_type_hint') + if token: + request.token = token + + uri, http_method, body, headers = extract_params() + ret = server.create_revocation_response( + uri, headers=headers, body=body, http_method=http_method) + return create_response(*ret) + return decorated + def require_oauth(self, *scopes): """Protect resource with specified scopes.""" def wrapper(f): @@ -879,3 +909,21 @@ def validate_user(self, username, password, client, request, return False log.debug('Password credential authorization is disabled.') return False + + def revoke_token(self, token, token_type_hint, request, *args, **kwargs): + """Revoke an access or refresh token. + """ + if token_type_hint: + tok = self._tokengetter(**{token_type_hint: token}) + else: + for token_type in ('access_token', 'refresh_token'): + tok = self._tokengetter(**{token_type: token}) + if tok: break + if tok: + tok.delete() + return True + + msg = 'Invalid token supplied.' + log.debug(msg) + request.error_message = msg + return False From b126b9388469441b38b1374af67ced7e76a156cc Mon Sep 17 00:00:00 2001 From: Misja Hoebe Date: Sun, 17 Aug 2014 01:01:13 +0200 Subject: [PATCH 100/279] Check ownership Adding an ownership check plus adding client_id and user to request. --- flask_oauthlib/provider/oauth2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 4d159c3e..6ad45c71 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -919,7 +919,10 @@ def revoke_token(self, token, token_type_hint, request, *args, **kwargs): for token_type in ('access_token', 'refresh_token'): tok = self._tokengetter(**{token_type: token}) if tok: break - if tok: + + if tok and tok.client_id == request.client.client_id: + request.client_id = token.client_id + request.user = token.user tok.delete() return True From 69fdeed8b514be31a0f5d3e0f214d76f8a195501 Mon Sep 17 00:00:00 2001 From: Misja Hoebe Date: Sun, 17 Aug 2014 01:09:54 +0200 Subject: [PATCH 101/279] Add tests for revoke endpoint --- tests/oauth2/server.py | 8 ++++++ tests/oauth2/test_oauth2.py | 57 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index 0c23b090..2fbc30db 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -116,6 +116,10 @@ def scopes(self): return self.scope.split() return [] + def delete(self): + db.session.delete(self) + db.session.commit() + return self def current_user(): return g.user @@ -270,6 +274,10 @@ def authorize(*args, **kwargs): def access_token(): return {} + @app.route('/oauth/revoke', methods=['POST']) + @oauth.revoke_handler + def revoke_token(): pass + @app.route('/api/email') @oauth.require_oauth('email') def email_api(): diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index 4c0de953..9b9c74b6 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -10,6 +10,7 @@ cache_provider, sqlalchemy_provider, default_provider, + Token ) from .client import create_client from .._base import BaseSuite, clean_url @@ -260,6 +261,62 @@ class TestRefreshTokenSQLAlchemy(TestRefreshToken): def create_oauth_provider(self, app): return sqlalchemy_provider(app) +class TestRevokeToken(OAuthSuite): + + def create_oauth_provider(self, app): + return default_provider(app) + + def get_token(self): + url = ('/oauth/token?grant_type=password' + '&scope=email+address&username=admin&password=admin') + rv = self.client.get(url, headers={ + 'Authorization': 'Basic %s' % auth_code, + }) + assert b'_token' in rv.data + return json.loads(u(rv.data)) + + def test_revoke_token(self): + data = self.get_token() + tok = Token.query.filter_by( + refresh_token=data['refresh_token']).first() + assert tok.refresh_token == data['refresh_token'] + + revoke_url = '/oauth/revoke' + args = {'token': data['refresh_token']} + rv = self.client.post(revoke_url, data=args, headers={ + 'Authorization': 'Basic %s' % auth_code, + }) + + tok = Token.query.filter_by( + refresh_token=data['refresh_token']).first() + assert tok == None + + def test_revoke_token_with_hint(self): + data = self.get_token() + tok = Token.query.filter_by( + access_token=data['access_token']).first() + assert tok.access_token == data['access_token'] + + revoke_url = '/oauth/revoke' + args = {'token': data['access_token'], + 'token_type_hint': 'access_token'} + rv = self.client.post(revoke_url, data=args, headers={ + 'Authorization': 'Basic %s' % auth_code, + }) + + tok = Token.query.filter_by( + access_token=data['access_token']).first() + assert tok == None + +class TestRevokeTokenCached(TestRefreshToken): + + def create_oauth_provider(self, app): + return cache_provider(app) + +class TestRevokeTokenSQLAlchemy(TestRefreshToken): + + def create_oauth_provider(self, app): + return sqlalchemy_provider(app) class TestCredentialAuth(OAuthSuite): From 2cdc2d6f3424ca333964dacb8b280afe0523e41d Mon Sep 17 00:00:00 2001 From: Misja Hoebe Date: Sun, 17 Aug 2014 01:31:49 +0200 Subject: [PATCH 102/279] Update documentation --- docs/oauth2.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/oauth2.rst b/docs/oauth2.rst index 09fa122a..c84cda76 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -169,6 +169,7 @@ A bearer token requires at least these information: - scopes: A list of scopes - expires: A `datetime.datetime` object - user: The user object +- delete: A function to delete itself An example of the data model in SQLAlchemy:: @@ -193,6 +194,11 @@ An example of the data model in SQLAlchemy:: expires = db.Column(db.DateTime) _scopes = db.Column(db.Text) + def delete(self): + db.session.delete(self) + db.session.commit() + return self + @property def scopes(self): if self._scopes: @@ -421,6 +427,18 @@ The authorization flow is finished, everything should be working now. and only available in password credential. +Revoke handler +`````````````` +In some cases a user may wish to revoke access given to an application and the +revoke handler makes it possible for an application to programmaticaly revoke +the access given to it. Also here you don't need to do much, allowing POST only +is recommended:: + + @app.route('/oauth/revoke', methods=['POST']) + @oauth.revoke_handler + def revoke_token(): pass + + Subclass way ```````````` From 79ba8ee84ff86a2284dea4b50c01811ebaf7fe02 Mon Sep 17 00:00:00 2001 From: Misja Hoebe Date: Sun, 17 Aug 2014 01:45:20 +0200 Subject: [PATCH 103/279] Use correct var for setting client_id and user plus trigger the decorated function after processing --- flask_oauthlib/provider/oauth2.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 6ad45c71..630d7e39 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -481,7 +481,7 @@ def revoke_handler(self, f): @app.route('/oauth/revoke', methods=['POST']) @oauth.revoke_handler def access_token(): pass - + .. _`RFC7009`: http://tools.ietf.org/html/rfc7009 """ @wraps(f) @@ -493,12 +493,12 @@ def decorated(*args, **kwargs): if token: request.token = token - uri, http_method, body, headers = extract_params() + uri, http_method, body, headers = extract_params() ret = server.create_revocation_response( uri, headers=headers, body=body, http_method=http_method) return create_response(*ret) return decorated - + def require_oauth(self, *scopes): """Protect resource with specified scopes.""" def wrapper(f): @@ -921,11 +921,11 @@ def revoke_token(self, token, token_type_hint, request, *args, **kwargs): if tok: break if tok and tok.client_id == request.client.client_id: - request.client_id = token.client_id - request.user = token.user + request.client_id = tok.client_id + request.user = tok.user tok.delete() return True - + msg = 'Invalid token supplied.' log.debug(msg) request.error_message = msg From c5b1296029fe123376cbd38c4a4f2acc43b6f6ee Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 18 Aug 2014 10:48:01 +0800 Subject: [PATCH 104/279] Stylefix for #131 --- flask_oauthlib/provider/oauth2.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 630d7e39..3f4e017e 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -472,15 +472,17 @@ def decorated(*args, **kwargs): def revoke_handler(self, f): """Access/refresh token revoke decorator. - Any return value by the decorated function will get discarded as defined - in [`RFC7009`_]. + Any return value by the decorated function will get discarded as + defined in [`RFC7009`_]. - You can control the access method with the standard flask routing mechanism, - as per [`RFC7009`_] it is recommended to only allow the `POST` method:: + You can control the access method with the standard flask routing + mechanism, as per [`RFC7009`_] it is recommended to only allow + the `POST` method:: @app.route('/oauth/revoke', methods=['POST']) @oauth.revoke_handler - def access_token(): pass + def revoke_token(): + pass .. _`RFC7009`: http://tools.ietf.org/html/rfc7009 """ @@ -916,9 +918,9 @@ def revoke_token(self, token, token_type_hint, request, *args, **kwargs): if token_type_hint: tok = self._tokengetter(**{token_type_hint: token}) else: - for token_type in ('access_token', 'refresh_token'): - tok = self._tokengetter(**{token_type: token}) - if tok: break + tok = self._tokengetter(access_token=token) + if not tok: + tok = self._tokengetter(refresh_token=token) if tok and tok.client_id == request.client.client_id: request.client_id = tok.client_id From 1dc26949ed9475a0ac40820c4a7b0d6fd92b5044 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 18 Aug 2014 10:51:28 +0800 Subject: [PATCH 105/279] Use authorized_response in documentation --- docs/client.rst | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/client.rst b/docs/client.rst index 8328f9da..655eacf2 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -108,18 +108,16 @@ redirected back to. For example:: return twitter.authorize(callback=url_for('oauth_authorized', next=request.args.get('next') or request.referrer or None)) -If the application redirects back, the remote application will have passed all -relevant information to the `oauth_authorized` function: a special -response object with all the data, or ``None`` if the user denied the -request. This function must be decorated as -:meth:`~OAuthRemoteApp.authorized_handler`:: +If the application redirects back, the remote application will can fetch +all relevant information in the `oauth_authorized` function with +:meth:`~OAuthRemoteApp.authorized_response`:: from flask import redirect @app.route('/oauth-authorized') - @twitter.authorized_handler - def oauth_authorized(resp): + def oauth_authorized(): next_url = request.args.get('next') or url_for('index') + resp = twitter.authorized_response() if resp is None: flash(u'You denied the request to sign in.') return redirect(next_url) From 2416e0c4b91333f551b5e194f712d38d34f8830f Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 18 Aug 2014 11:31:19 +0800 Subject: [PATCH 106/279] Deprecated @authorized_handler in favor of authorized_response. --- flask_oauthlib/client.py | 10 +++++++++- tests/oauth1/client.py | 4 ++-- tests/oauth2/client.py | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 4fc13fe2..e509a2ae 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -640,9 +640,17 @@ def authorized_response(self): return data def authorized_handler(self, f): - """Handles an OAuth callback.""" + """Handles an OAuth callback. + + .. versionchanged:: 0.7 + @authorized_handler is deprecated in favor for authorized_response. + """ @wraps(f) def decorated(*args, **kwargs): + log.warn( + '@authorized_handler is deprecated in favor for ' + 'authorized_response' + ) data = self.authorized_response() return f(*((data,) + args), **kwargs) return decorated diff --git a/tests/oauth1/client.py b/tests/oauth1/client.py index 9c915fc1..d3e1ffce 100644 --- a/tests/oauth1/client.py +++ b/tests/oauth1/client.py @@ -42,8 +42,8 @@ def logout(): return redirect(url_for('index')) @app.route('/authorized') - @oauth.authorized_handler - def authorized(resp): + def authorized(): + resp = oauth.authorized_response() if resp is None: return 'Access denied: error=%s' % ( request.args['error'] diff --git a/tests/oauth2/client.py b/tests/oauth2/client.py index 9affa60f..904d9cd1 100644 --- a/tests/oauth2/client.py +++ b/tests/oauth2/client.py @@ -34,8 +34,8 @@ def logout(): return redirect(url_for('index')) @app.route('/authorized') - @remote.authorized_handler - def authorized(resp): + def authorized(): + resp = remote.authorized_response() if resp is None: return 'Access denied: error=%s' % ( request.args['error'] From 92cf9eb511efc92c86972a67d8088293d3c62f96 Mon Sep 17 00:00:00 2001 From: Kevin McCarthy Date: Thu, 14 Aug 2014 20:29:35 +0000 Subject: [PATCH 107/279] Fix issue with Werkzeug and weird query string urldecoding Werkzeug's request object has a url method that returns a URL with a partially-decoded querystring, causing some methods to fail in weird ways with a message about urlencoding. This patch fixes the problem for me. I have also added a couple tests for the method in the utils model that extracts the params from the request object since it was untested. The second test is the regresssion test that reproduces the bug. I have also removed a test that I think is incorrect. See my comments on the commit here: https://github.com/lepture/flask-oauthlib/commit/7102484e423686e9bc31b32688531b5fa4e827e9 --- flask_oauthlib/utils.py | 15 ++++++++++++- tests/oauth1/test_oauth1.py | 6 ----- tests/test_utils.py | 45 +++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 tests/test_utils.py diff --git a/flask_oauthlib/utils.py b/flask_oauthlib/utils.py index 23cc14d7..f5921309 100644 --- a/flask_oauthlib/utils.py +++ b/flask_oauthlib/utils.py @@ -5,9 +5,22 @@ from oauthlib.common import to_unicode, bytes_type +def _get_uri_from_request(request): + """ + The uri returned from request.uri is not properly urlencoded + (sometimes it's partially urldecoded) This is a weird hack to get + werkzeug to return the proper urlencoded string uri + """ + uri = request.base_url + if request.query_string: + uri += '?' + request.query_string.decode('utf-8') + return uri + + def extract_params(): """Extract request params.""" - uri = request.url + + uri = _get_uri_from_request(request) http_method = request.method headers = dict(request.headers) if 'wsgi.input' in headers: diff --git a/tests/oauth1/test_oauth1.py b/tests/oauth1/test_oauth1.py index d2ab0bf8..ed7d0fe0 100644 --- a/tests/oauth1/test_oauth1.py +++ b/tests/oauth1/test_oauth1.py @@ -95,12 +95,6 @@ def test_invalid_request_token(self): }) assert 'error' in rv.location - def test_invalid_urlencoded(self): - rv = self.client.get('/oauth/request_token?query=tam%20q') - assert b'non+urlencoded' in rv.data - rv = self.client.get('/oauth/access_token?query=tam%20q') - assert b'non+urlencoded' in rv.data - auth_header = ( u'OAuth realm="%(realm)s",' diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..68299ebd --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,45 @@ +import unittest +import wsgiref.util +from contextlib import contextmanager +import mock +import werkzeug.wrappers +from flask_oauthlib.utils import extract_params +from oauthlib.common import Request +from flask import request + + +@contextmanager +def set_flask_request(wsgi_environ): + """ + Test helper context manager that mocks the flask request global I didn't + need the whole request context just to test the functions in helpers and I + wanted to be able to set the raw WSGI environment + """ + environ = {} + environ.update(wsgi_environ) + wsgiref.util.setup_testing_defaults(environ) + r = werkzeug.wrappers.Request(environ) + + with mock.patch.dict(extract_params.__globals__, {'request': r}): + yield + + +class UtilsTestSuite(unittest.TestCase): + + def test_extract_params(self): + with set_flask_request({'QUERY_STRING': 'test=foo&foo=bar'}): + uri, http_method, body, headers = extract_params() + self.assertEquals(uri, 'http://127.0.0.1/?test=foo&foo=bar') + self.assertEquals(http_method, 'GET') + self.assertEquals(body, {}) + self.assertEquals(headers, {'Host': '127.0.0.1'}) + + def test_extract_params_with_urlencoded_json(self): + wsgi_environ = { + 'QUERY_STRING': 'state=%7B%22t%22%3A%22a%22%2C%22i%22%3A%22l%22%7D' + } + with set_flask_request(wsgi_environ): + uri, http_method, body, headers = extract_params() + # Request constructor will try to urldecode the querystring, make + # sure this doesn't fail. + Request(uri, http_method, body, headers) From 61853f40cebbc3076dc1e748895b7dd20974b237 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 18 Aug 2014 14:22:35 +0800 Subject: [PATCH 108/279] Prepare for the next release of 0.7 --- docs/changelog.rst | 15 +++++++++++++++ flask_oauthlib/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index fbec2243..26767920 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,21 @@ Changelog Here you can see the full list of changes between each Flask-OAuthlib release. +Version 0.7.0 +------------- + +Release date to be decided. + +.. module:: flask_oauthlib.client + +- Deprecated :meth:`OAuthRemoteApp.authorized_handler` in favor of + :meth:`OAuthRemoteApp.authorized_response`. +- Add revocation endpoint via `#131`_. +- Handle unknown exceptions in providers. + +.. _`#131`: https://github.com/lepture/flask-oauthlib/pull/131 + + Version 0.6.0 ------------- diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index 206f3812..36dbfdcc 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -11,7 +11,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = "0.6.0" +__version__ = "0.7.0" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' diff --git a/setup.py b/setup.py index c2e476e4..3abff62d 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ def fread(filename): tests_require=['nose', 'Flask-SQLAlchemy', 'mock'], test_suite='nose.collector', classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved', From 9d606dec55b6baf53c63b6794173b104bb1403fc Mon Sep 17 00:00:00 2001 From: Misja Hoebe Date: Wed, 20 Aug 2014 10:04:38 +0200 Subject: [PATCH 109/279] Add HTTP PATCH method and typo fixes --- flask_oauthlib/client.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index e509a2ae..5a34d74e 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -357,33 +357,40 @@ def http_request(uri, headers=None, data=None, method=None): return resp, content def get(self, *args, **kwargs): - """Sends a ``GET`` request. Accepts the same paramters as + """Sends a ``GET`` request. Accepts the same parameters as :meth:`request`. """ kwargs['method'] = 'GET' return self.request(*args, **kwargs) def post(self, *args, **kwargs): - """Sends a ``POST`` request. Accepts the same paramters as + """Sends a ``POST`` request. Accepts the same parameters as :meth:`request`. """ kwargs['method'] = 'POST' return self.request(*args, **kwargs) def put(self, *args, **kwargs): - """Sends a ``PUT`` request. Accepts the same paramters as + """Sends a ``PUT`` request. Accepts the same parameters as :meth:`request`. """ kwargs['method'] = 'PUT' return self.request(*args, **kwargs) def delete(self, *args, **kwargs): - """Sends a ``DELETE`` request. Accepts the same paramters as + """Sends a ``DELETE`` request. Accepts the same parameters as :meth:`request`. """ kwargs['method'] = 'DELETE' return self.request(*args, **kwargs) + def patch(self, *args, **kwargs): + """Sends a ``PATCH`` request. Accepts the same parameters as + :meth:`post`. + """ + kwargs['method'] = 'PATCH' + return self.request(*args, **kwargs) + def request(self, url, data=None, headers=None, format='urlencoded', method='GET', content_type=None, token=None): """ From e99e52169e26af6a8088f57889acea8f5f51d177 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 25 Aug 2014 10:47:09 +0800 Subject: [PATCH 110/279] Version bump 0.7.0 --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 26767920..b89de5ce 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,8 +16,10 @@ Release date to be decided. :meth:`OAuthRemoteApp.authorized_response`. - Add revocation endpoint via `#131`_. - Handle unknown exceptions in providers. +- Add PATCH method for client via `134`_. .. _`#131`: https://github.com/lepture/flask-oauthlib/pull/131 +.. _`#134`: https://github.com/lepture/flask-oauthlib/pull/134 Version 0.6.0 From faa4eed793d2e5d43de9bd9de9c0229e3670cd30 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 26 Aug 2014 21:03:34 +0800 Subject: [PATCH 111/279] Mark Exception as unknown error. https://github.com/lepture/flask-oauthlib/issues/137 --- flask_oauthlib/provider/oauth1.py | 14 +++++++------- flask_oauthlib/provider/oauth2.py | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index 8ed9964f..4405f543 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -408,9 +408,9 @@ def decorated(*args, **kwargs): return redirect(e.in_uri(self.error_uri)) except errors.InvalidClientError as e: return redirect(e.in_uri(self.error_uri)) - except Exception as e: + except Exception: return redirect(add_params_to_uri( - self.error_uri, {'error': e.message} + self.error_uri, {'error': 'unknown'} )) return decorated @@ -432,9 +432,9 @@ def confirm_authorization_request(self): return redirect(e.in_uri(self.error_uri)) except errors.InvalidClientError as e: return redirect(e.in_uri(self.error_uri)) - except Exception as e: + except Exception: return redirect(add_params_to_uri( - self.error_uri, {'error': e.message} + self.error_uri, {'error': 'unknown'} )) def request_token_handler(self, f): @@ -463,7 +463,7 @@ def decorated(*args, **kwargs): except errors.OAuth1Error as e: return _error_response(e) except Exception as e: - e.urlencoded = urlencode([('error', str(e))]) + e.urlencoded = urlencode([('error', 'unknown')]) e.status_code = 400 return _error_response(e) return decorated @@ -494,7 +494,7 @@ def decorated(*args, **kwargs): except errors.OAuth1Error as e: return _error_response(e) except Exception as e: - e.urlencoded = urlencode([('error', str(e))]) + e.urlencoded = urlencode([('error', 'unknown')]) e.status_code = 400 return _error_response(e) return decorated @@ -517,7 +517,7 @@ def decorated(*args, **kwargs): uri, http_method, body, headers, realms ) except Exception as e: - e.urlencoded = urlencode([('error', str(e))]) + e.urlencoded = urlencode([('error', 'unknown')]) e.status_code = 400 return _error_response(e) for func in self._after_request_funcs: diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 3f4e017e..4ba97943 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -384,9 +384,9 @@ def decorated(*args, **kwargs): except oauth2.FatalClientError as e: log.debug('Fatal client error %r', e) return redirect(e.in_uri(self.error_uri)) - except Exception as e: + except Exception: return redirect(add_params_to_uri( - self.error_uri, {'error': e.message} + self.error_uri, {'error': 'unknown'} )) else: @@ -397,9 +397,9 @@ def decorated(*args, **kwargs): except oauth2.FatalClientError as e: log.debug('Fatal client error %r', e) return redirect(e.in_uri(self.error_uri)) - except Exception as e: + except Exception: return redirect(add_params_to_uri( - self.error_uri, {'error': e.message} + self.error_uri, {'error': 'unknown'} )) if not isinstance(rv, bool): @@ -438,9 +438,9 @@ def confirm_authorization_request(self): return redirect(e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: return redirect(e.in_uri(redirect_uri)) - except Exception as e: + except Exception: return redirect(add_params_to_uri( - self.error_uri, {'error': e.message} + self.error_uri, {'error': 'unknown'} )) def token_handler(self, f): From 8a8d09215446c7bcb7bc5c42fc6f2dbf46ccb756 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 26 Aug 2014 21:05:59 +0800 Subject: [PATCH 112/279] Log warn the Exception --- flask_oauthlib/provider/oauth1.py | 9 +++++++-- flask_oauthlib/provider/oauth2.py | 9 ++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index 4405f543..3cdb26d0 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -408,7 +408,8 @@ def decorated(*args, **kwargs): return redirect(e.in_uri(self.error_uri)) except errors.InvalidClientError as e: return redirect(e.in_uri(self.error_uri)) - except Exception: + except Exception as e: + log.warn('Exception: %r', e) return redirect(add_params_to_uri( self.error_uri, {'error': 'unknown'} )) @@ -432,7 +433,8 @@ def confirm_authorization_request(self): return redirect(e.in_uri(self.error_uri)) except errors.InvalidClientError as e: return redirect(e.in_uri(self.error_uri)) - except Exception: + except Exception as e: + log.warn('Exception: %r', e) return redirect(add_params_to_uri( self.error_uri, {'error': 'unknown'} )) @@ -463,6 +465,7 @@ def decorated(*args, **kwargs): except errors.OAuth1Error as e: return _error_response(e) except Exception as e: + log.warn('Exception: %r', e) e.urlencoded = urlencode([('error', 'unknown')]) e.status_code = 400 return _error_response(e) @@ -494,6 +497,7 @@ def decorated(*args, **kwargs): except errors.OAuth1Error as e: return _error_response(e) except Exception as e: + log.warn('Exception: %r', e) e.urlencoded = urlencode([('error', 'unknown')]) e.status_code = 400 return _error_response(e) @@ -517,6 +521,7 @@ def decorated(*args, **kwargs): uri, http_method, body, headers, realms ) except Exception as e: + log.warn('Exception: %r', e) e.urlencoded = urlencode([('error', 'unknown')]) e.status_code = 400 return _error_response(e) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 4ba97943..bb663c3c 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -384,7 +384,8 @@ def decorated(*args, **kwargs): except oauth2.FatalClientError as e: log.debug('Fatal client error %r', e) return redirect(e.in_uri(self.error_uri)) - except Exception: + except Exception as e: + log.warn('Exception: %r', e) return redirect(add_params_to_uri( self.error_uri, {'error': 'unknown'} )) @@ -397,7 +398,8 @@ def decorated(*args, **kwargs): except oauth2.FatalClientError as e: log.debug('Fatal client error %r', e) return redirect(e.in_uri(self.error_uri)) - except Exception: + except Exception as e: + log.warn('Exception: %r', e) return redirect(add_params_to_uri( self.error_uri, {'error': 'unknown'} )) @@ -438,7 +440,8 @@ def confirm_authorization_request(self): return redirect(e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: return redirect(e.in_uri(redirect_uri)) - except Exception: + except Exception as e: + log.warn('Exception: %r', e) return redirect(add_params_to_uri( self.error_uri, {'error': 'unknown'} )) From 3f766bb78aa2b43b3e178a1a5933b4085bae0e7f Mon Sep 17 00:00:00 2001 From: Lars Holm Nielsen Date: Wed, 27 Aug 2014 15:11:01 +0200 Subject: [PATCH 113/279] Notify client of OAuth2Errors * Properly report non-fatal client errors to the client (like e.g. invalid_scope) according to http://tools.ietf.org/html/rfc6749#section-4.1.2.1 --- flask_oauthlib/provider/oauth2.py | 12 ++++++++++-- tests/oauth2/test_oauth2.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index bb663c3c..ebb3763d 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -384,6 +384,9 @@ def decorated(*args, **kwargs): except oauth2.FatalClientError as e: log.debug('Fatal client error %r', e) return redirect(e.in_uri(self.error_uri)) + except oauth2.OAuth2Error as e: + log.debug('OAuth2Error: %r', e) + return redirect(e.in_uri(redirect_uri or self.error_uri)) except Exception as e: log.warn('Exception: %r', e) return redirect(add_params_to_uri( @@ -398,6 +401,9 @@ def decorated(*args, **kwargs): except oauth2.FatalClientError as e: log.debug('Fatal client error %r', e) return redirect(e.in_uri(self.error_uri)) + except oauth2.OAuth2Error as e: + log.debug('OAuth2Error: %r', e) + return redirect(e.in_uri(redirect_uri or self.error_uri)) except Exception as e: log.warn('Exception: %r', e) return redirect(add_params_to_uri( @@ -416,7 +422,7 @@ def decorated(*args, **kwargs): return decorated def confirm_authorization_request(self): - """When consumer confirm the authrozation.""" + """When consumer confirm the authorization.""" server = self.server scope = request.values.get('scope') or '' scopes = scope.split() @@ -437,9 +443,11 @@ def confirm_authorization_request(self): log.debug('Authorization successful.') return create_response(*ret) except oauth2.FatalClientError as e: + log.debug('Fatal client error %r', e) return redirect(e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: - return redirect(e.in_uri(redirect_uri)) + log.debug('OAuth2Error: %r', e) + return redirect(e.in_uri(redirect_uri or self.error_uri)) except Exception as e: log.warn('Exception: %r', e) return redirect(add_params_to_uri( diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index 9b9c74b6..ec14cd7b 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -162,6 +162,17 @@ def test_invalid_response_type(self): rv = self.client.get(clean_url(http://github.com/rv.location)) assert b'error' in rv.data + def test_invalid_scope(self): + authorize_url = ( + '/oauth/authorize?response_type=code&client_id=dev' + '&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fauthorized' + '&scope=invalid' + ) + rv = self.client.get(authorize_url) + rv = self.client.get(clean_url(http://github.com/rv.location)) + assert b'error' in rv.data + assert b'invalid_scope' in rv.data + class TestWebAuthCached(TestWebAuth): From 0f3de0bdb296aac34350a7d88703ca0d5526d707 Mon Sep 17 00:00:00 2001 From: Lars Holm Nielsen Date: Wed, 27 Aug 2014 15:22:33 +0200 Subject: [PATCH 114/279] Fix all lint and PEP8 errors --- tests/oauth1/server.py | 2 +- tests/oauth2/server.py | 6 ++++-- tests/oauth2/test_oauth2.py | 16 ++++++++++------ tests/test_utils.py | 1 - 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/oauth1/server.py b/tests/oauth1/server.py index 51c5d084..917ed1e9 100644 --- a/tests/oauth1/server.py +++ b/tests/oauth1/server.py @@ -24,7 +24,7 @@ class User(db.Model): class Client(db.Model): - #id = db.Column(db.Integer, primary_key=True) + # id = db.Column(db.Integer, primary_key=True) # human readable name client_key = db.Column(db.String(40), primary_key=True) client_secret = db.Column(db.String(55), unique=True, index=True, diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index 2fbc30db..f1e689e6 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -21,7 +21,7 @@ def check_password(self, password): class Client(db.Model): - #id = db.Column(db.Integer, primary_key=True) + # id = db.Column(db.Integer, primary_key=True) # human readable name name = db.Column(db.String(40)) client_id = db.Column(db.String(40), primary_key=True) @@ -121,6 +121,7 @@ def delete(self): db.session.commit() return self + def current_user(): return g.user @@ -276,7 +277,8 @@ def access_token(): @app.route('/oauth/revoke', methods=['POST']) @oauth.revoke_handler - def revoke_token(): pass + def revoke_token(): + pass @app.route('/api/email') @oauth.require_oauth('email') diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index ec14cd7b..862cdef7 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -272,6 +272,7 @@ class TestRefreshTokenSQLAlchemy(TestRefreshToken): def create_oauth_provider(self, app): return sqlalchemy_provider(app) + class TestRevokeToken(OAuthSuite): def create_oauth_provider(self, app): @@ -288,47 +289,50 @@ def get_token(self): def test_revoke_token(self): data = self.get_token() - tok = Token.query.filter_by( + tok = Token.query.filter_by( refresh_token=data['refresh_token']).first() assert tok.refresh_token == data['refresh_token'] revoke_url = '/oauth/revoke' args = {'token': data['refresh_token']} - rv = self.client.post(revoke_url, data=args, headers={ + self.client.post(revoke_url, data=args, headers={ 'Authorization': 'Basic %s' % auth_code, }) tok = Token.query.filter_by( refresh_token=data['refresh_token']).first() - assert tok == None + assert tok is None def test_revoke_token_with_hint(self): data = self.get_token() - tok = Token.query.filter_by( + tok = Token.query.filter_by( access_token=data['access_token']).first() assert tok.access_token == data['access_token'] revoke_url = '/oauth/revoke' args = {'token': data['access_token'], 'token_type_hint': 'access_token'} - rv = self.client.post(revoke_url, data=args, headers={ + self.client.post(revoke_url, data=args, headers={ 'Authorization': 'Basic %s' % auth_code, }) tok = Token.query.filter_by( access_token=data['access_token']).first() - assert tok == None + assert tok is None + class TestRevokeTokenCached(TestRefreshToken): def create_oauth_provider(self, app): return cache_provider(app) + class TestRevokeTokenSQLAlchemy(TestRefreshToken): def create_oauth_provider(self, app): return sqlalchemy_provider(app) + class TestCredentialAuth(OAuthSuite): def create_oauth_provider(self, app): diff --git a/tests/test_utils.py b/tests/test_utils.py index 68299ebd..c3cb8f5a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,7 +5,6 @@ import werkzeug.wrappers from flask_oauthlib.utils import extract_params from oauthlib.common import Request -from flask import request @contextmanager From 602f23819b29fbeaa9b0c48507a3d0fcbb1516c6 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 29 Aug 2014 15:26:42 -0400 Subject: [PATCH 115/279] Attach response data to OAuthException --- flask_oauthlib/client.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 5a34d74e..2b78a8bd 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -530,17 +530,21 @@ def generate_request_token(self, callback=None): ) log.debug('Generate request token header %r', headers) resp, content = self.http_request(uri, headers) - if resp.code not in (200, 201): - raise OAuthException( - 'Failed to generate request token', - type='token_generation_failed' - ) data = parse_response(resp, content) - if data is None: + if not data: raise OAuthException( 'Invalid token response from %s' % self.name, type='token_generation_failed' ) + if resp.code not in (200, 201): + message = 'Failed to generate request token' + if 'oauth_problem' in data: + message += ' (%s)' % data['oauth_problem'] + raise OAuthException( + message, + type='token_generation_failed', + data=data, + ) tup = (data['oauth_token'], data['oauth_token_secret']) session['%s_oauthtok' % self.name] = tup return tup From e092a5f46ff6c78d313a6eb2f89ee746fb190dfb Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 2 Sep 2014 13:57:38 -0400 Subject: [PATCH 116/279] Pass request_token_params to init, rather than setting afterward --- flask_oauthlib/client.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 2b78a8bd..e40042a2 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -315,20 +315,16 @@ def _get_property(self, key, default=False): def make_client(self, token=None): # request_token_url is for oauth1 if self.request_token_url: + params = self.request_token_params or {} + if token and isinstance(token, (tuple, list)): + params["resource_owner_key"] = token[0] + params["resource_owner_secret"] = token[1] + client = oauthlib.oauth1.Client( - self.consumer_key, self.consumer_secret + self.consumer_key, self.consumer_secret, + **params ) - params = self.request_token_params or {} - if 'signature_method' in params: - client.signature_method = _encode(params['signature_method']) - if 'rsa_key' in params: - client.rsa_key = _encode(params['rsa_key']) - if 'signature_type' in params: - client.signature_type = _encode(params['signature_type']) - - if token and isinstance(token, (tuple, list)): - client.resource_owner_key, client.resource_owner_secret = token else: if token and isinstance(token, (tuple, list)): token = {'access_token': token[0]} From 9a45b0e3025373613b23b9f120c2c61e68fbc6e4 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 2 Sep 2014 10:50:43 -0400 Subject: [PATCH 117/279] Refactor required authentication info check The client_secret is unnecessary for RSA-based authentication, so don't require it. --- flask_oauthlib/client.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index e40042a2..50a1b931 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -231,11 +231,6 @@ def __init__( self.oauth = oauth self.name = name - if (not consumer_key or not consumer_secret) and not app_key: - raise TypeError( - 'OAuthRemoteApp requires consumer key and secret' - ) - self._base_url = base_url self._request_token_url = request_token_url self._access_token_url = access_token_url @@ -251,6 +246,26 @@ def __init__( self.app_key = app_key self.encoding = encoding + # Check for required authentication information. + # Skip this check if app_key is specified, since the information is + # specified in the Flask config, instead. + if not app_key: + req_params = self.request_token_params or {} + if req_params.get("signature_method") == oauthlib.oauth1.SIGNATURE_RSA: + # check for consumer_key and rsa_key + rsa_key = req_params.get("rsa_key") + if not consumer_key or not rsa_key: + raise TypeError( + "OAuthRemoteApp with RSA authentication requires " + "consumer key and rsa key" + ) + else: + # check for consumer_key and consumer_secret + if not consumer_key or not consumer_secret: + raise TypeError( + "OAuthRemoteApp requires consumer key and secret" + ) + @cached_property def base_url(http://github.com/self): return self._get_property('base_url') @@ -321,7 +336,8 @@ def make_client(self, token=None): params["resource_owner_secret"] = token[1] client = oauthlib.oauth1.Client( - self.consumer_key, self.consumer_secret, + client_key=self.consumer_key, + client_secret=self.consumer_secret, **params ) From 204275c450f0f4160d90adbfc780cc3bcbbc4492 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 2 Sep 2014 10:38:51 -0400 Subject: [PATCH 118/279] OAuth provider can specify alternate versions of the HTTP method to obtain a request token http://oauth.net/core/1.0/#rfc.section.6.1.1 --- flask_oauthlib/client.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 50a1b931..846036d1 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -200,6 +200,8 @@ class OAuthRemoteApp(object): to forward to the request token url or authorize url depending on oauth version + :param request_token_method: the HTTP method that should be used for + the access_token_url. Default is ``GET`` :param access_token_params: an optional dictionary of parameters to forward to the access token url :param access_token_method: the HTTP method that should be used for @@ -222,6 +224,7 @@ def __init__( consumer_key=None, consumer_secret=None, request_token_params=None, + request_token_method=None, access_token_params=None, access_token_method=None, content_type=None, @@ -238,6 +241,7 @@ def __init__( self._consumer_key = consumer_key self._consumer_secret = consumer_secret self._request_token_params = request_token_params + self._request_token_method = request_token_method self._access_token_params = access_token_params self._access_token_method = access_token_method self._content_type = content_type @@ -294,6 +298,10 @@ def consumer_secret(self): def request_token_params(self): return self._get_property('request_token_params', {}) + @cached_property + def request_token_method(self): + return self._get_property('request_token_method', 'GET') + @cached_property def access_token_params(self): return self._get_property('access_token_params', {}) @@ -538,10 +546,14 @@ def generate_request_token(self, callback=None): if not realm and realms: realm = ' '.join(realms) uri, headers, _ = client.sign( - self.expand_url(http://github.com/self.request_token_url), realm=realm + self.expand_url(http://github.com/self.request_token_url), + http_method=self.request_token_method, + realm=realm, ) log.debug('Generate request token header %r', headers) - resp, content = self.http_request(uri, headers) + resp, content = self.http_request( + uri, headers, method=self.request_token_method, + ) data = parse_response(resp, content) if not data: raise OAuthException( From 448ae210c0448d88641d01018e88e7a39225a60a Mon Sep 17 00:00:00 2001 From: mdxs Date: Thu, 11 Sep 2014 21:58:53 +0200 Subject: [PATCH 119/279] Small textual changes in README Some small textual changes in README: grammar and typos. --- README.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 36340692..33c05c43 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ Flask-OAuthlib Flask-OAuthlib is an extension to Flask that allows you to interact with remote OAuth enabled applications. On the client site, it is a replacement for Flask-OAuth. But it does more than that, it also helps you to create -oauth providers. +OAuth providers. Flask-OAuthlib relies on oauthlib_. @@ -21,7 +21,7 @@ Features -------- - Support for OAuth 1.0a, 1.0, 1.1, OAuth2 client -- Friendly API (same with Flask-OAuth) +- Friendly API (same as Flask-OAuth) - Direct integration with Flask - Basic support for remote method invocation of RESTful APIs - Support OAuth1 provider with HMAC and RSA signature @@ -35,7 +35,7 @@ And request more features at `github issues`_. Installation ------------ -Install flask-oauthlib is simple with pip_:: +Installing flask-oauthlib is simple with pip_:: $ pip install Flask-OAuthlib @@ -49,11 +49,11 @@ If you don't have pip installed, try with easy_install:: Additional Notes ---------------- -We keep a documentation at `flask-oauthlib@readthedocs`_. +We keep documentation at `flask-oauthlib@readthedocs`_. .. _`flask-oauthlib@readthedocs`: https://flask-oauthlib.readthedocs.org -If you are only interested in client part, you can find some examples +If you are only interested in the client part, you can find some examples in the ``example`` directory. There is also a `development version `_ on GitHub. From 26fc571c46194c2b8484f2d4f86b442c39906e01 Mon Sep 17 00:00:00 2001 From: mdxs Date: Mon, 15 Sep 2014 09:55:33 +0200 Subject: [PATCH 120/279] Small documentation fix --- docs/client.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/client.rst b/docs/client.rst index 655eacf2..bb459f13 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -108,7 +108,7 @@ redirected back to. For example:: return twitter.authorize(callback=url_for('oauth_authorized', next=request.args.get('next') or request.referrer or None)) -If the application redirects back, the remote application will can fetch +If the application redirects back, the remote application can fetch all relevant information in the `oauth_authorized` function with :meth:`~OAuthRemoteApp.authorized_response`:: @@ -132,7 +132,7 @@ all relevant information in the `oauth_authorized` function with return redirect(next_url) We store the token and the associated secret in the session so that the -tokengetter can return it. Additionally we also store the Twitter username +tokengetter can return it. Additionally, we also store the Twitter username that was sent back to us so that we can later display it to the user. In larger applications it is recommended to store satellite information in a database instead to ease debugging and more easily handle additional information From cfad30a99627fb1518dd61f96e61ae12f73163be Mon Sep 17 00:00:00 2001 From: mdxs Date: Mon, 15 Sep 2014 09:59:41 +0200 Subject: [PATCH 121/279] Textual improvement Using `in favor of` instead of `in favor for`; see http://en.wiktionary.org/wiki/in_favor_of --- flask_oauthlib/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 846036d1..a626cf9d 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -678,12 +678,12 @@ def authorized_handler(self, f): """Handles an OAuth callback. .. versionchanged:: 0.7 - @authorized_handler is deprecated in favor for authorized_response. + @authorized_handler is deprecated in favor of authorized_response. """ @wraps(f) def decorated(*args, **kwargs): log.warn( - '@authorized_handler is deprecated in favor for ' + '@authorized_handler is deprecated in favor of ' 'authorized_response' ) data = self.authorized_response() From 6479a7900fe89eb38e336c0e530f4b6d4bed4f91 Mon Sep 17 00:00:00 2001 From: mdxs Date: Mon, 15 Sep 2014 13:54:15 +0200 Subject: [PATCH 122/279] Updated error handling for Dropbox example According to https://www.dropbox.com/developers/core/docs#oa2-authorize there is no `error_reason` but rather an `error` code (per Section 4.1.2.1 of the OAuth 2.0 spec) --- example/dropbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/dropbox.py b/example/dropbox.py index 6bbc8808..d247215b 100644 --- a/example/dropbox.py +++ b/example/dropbox.py @@ -44,7 +44,7 @@ def authorized(): resp = dropbox.authorized_response() if resp is None: return 'Access denied: reason=%s error=%s' % ( - request.args['error_reason'], + request.args['error'], request.args['error_description'] ) session['dropbox_token'] = (resp['access_token'], '') From ea47536124c26d3edcfdeb5b05dc122dadb586bd Mon Sep 17 00:00:00 2001 From: Lauri Andler Date: Tue, 16 Sep 2014 11:55:08 +0300 Subject: [PATCH 123/279] Removed client secret checking from authenticate_client_id Change client_authentication_required to return False if client_type is not confidential (unless client_secret is provided). --- flask_oauthlib/provider/oauth2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index ebb3763d..b402fe6e 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -574,7 +574,10 @@ def client_authentication_required(self, request, *args, **kwargs): .. _`Section 4.1.3`: http://tools.ietf.org/html/rfc6749#section-4.1.3 .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 """ - if request.grant_type == 'password': + + client = self._clientgetter(request.client_id) + + if request.grant_type == 'password' and (client.client_type == 'confidential' or request.client_secret): return True auth_required = ('authorization_code', 'refresh_token') return 'Authorization' in request.headers and\ @@ -631,9 +634,6 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): log.debug('Authenticate failed, client not found.') return False - if client.client_secret != request.client_secret: - log.debug('Authenticate client failed, secret not match.') - return False # attach client on request for convenience request.client = client From a504ccadf27e2d9b2dd6496660182326b01a288a Mon Sep 17 00:00:00 2001 From: Lauri Andler Date: Tue, 16 Sep 2014 13:08:47 +0300 Subject: [PATCH 124/279] Fixed logic - now tests pass --- flask_oauthlib/provider/oauth2.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index b402fe6e..67563760 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -575,10 +575,14 @@ def client_authentication_required(self, request, *args, **kwargs): .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 """ - client = self._clientgetter(request.client_id) + + if(request.grant_type=='password'): + client = self._clientgetter(request.client_id) + if (not client) or client.client_type == 'confidential' or request.client_secret: + return True + else: + return False - if request.grant_type == 'password' and (client.client_type == 'confidential' or request.client_secret): - return True auth_required = ('authorization_code', 'refresh_token') return 'Authorization' in request.headers and\ request.grant_type in auth_required From 6e924333cf70a9ca0a4135179864cd51ecbd04b5 Mon Sep 17 00:00:00 2001 From: Lauri Andler Date: Tue, 16 Sep 2014 13:15:36 +0300 Subject: [PATCH 125/279] Fixed lint errors --- flask_oauthlib/provider/oauth2.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 67563760..16973591 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -575,10 +575,10 @@ def client_authentication_required(self, request, *args, **kwargs): .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 """ - - if(request.grant_type=='password'): + if request.grant_type == 'password': client = self._clientgetter(request.client_id) - if (not client) or client.client_type == 'confidential' or request.client_secret: + if (not client) or client.client_type == 'confidential' or\ + request.client_secret: return True else: return False @@ -638,7 +638,6 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): log.debug('Authenticate failed, client not found.') return False - # attach client on request for convenience request.client = client return True From 33f155bb86c5482b638b7383fb360d02b11a99f1 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 17 Sep 2014 17:17:15 +0800 Subject: [PATCH 126/279] Return instead of if-else --- flask_oauthlib/provider/oauth2.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 16973591..ea0a0eaa 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -577,11 +577,8 @@ def client_authentication_required(self, request, *args, **kwargs): if request.grant_type == 'password': client = self._clientgetter(request.client_id) - if (not client) or client.client_type == 'confidential' or\ - request.client_secret: - return True - else: - return False + return (not client) or client.client_type == 'confidential' or\ + request.client_secret auth_required = ('authorization_code', 'refresh_token') return 'Authorization' in request.headers and\ From 069dd0aa00910f1ff33c8f40c27e239179c74102 Mon Sep 17 00:00:00 2001 From: Victor Boivie Date: Tue, 23 Sep 2014 07:48:03 +0200 Subject: [PATCH 127/279] Support for generating refresh tokens Due to 5d0fc1182f6d9826764cc43ade99bc5e5a81c711 in oauthlib, there are now separate generators for access tokens and refresh tokens. Adding a hook to also specify refresh token generator. Test cases are added. --- flask_oauthlib/provider/oauth2.py | 8 ++++++++ tests/oauth2/test_oauth2.py | 24 +++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index ea0a0eaa..5bf114e6 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -131,11 +131,18 @@ def validate_client_id(self, client_id): if token_generator and not callable(token_generator): token_generator = import_string(token_generator) + refresh_token_generator = self.app.config.get( + 'OAUTH2_PROVIDER_REFRESH_TOKEN_GENERATOR', None + ) + if refresh_token_generator and not callable(refresh_token_generator): + refresh_token_generator = import_string(refresh_token_generator) + if hasattr(self, '_validator'): return Server( self._validator, token_expires_in=expires_in, token_generator=token_generator, + refresh_token_generator=refresh_token_generator, ) if hasattr(self, '_clientgetter') and \ @@ -161,6 +168,7 @@ def validate_client_id(self, client_id): validator, token_expires_in=expires_in, token_generator=token_generator, + refresh_token_generator=refresh_token_generator, ) raise RuntimeError('application not bound to required getters') diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index 862cdef7..abd10cc1 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -389,7 +389,7 @@ class TestTokenGenerator(OAuthSuite): def create_oauth_provider(self, app): - def generator(request, refresh_token=False): + def generator(request): return 'foobar' app.config['OAUTH2_PROVIDER_TOKEN_GENERATOR'] = generator @@ -403,6 +403,28 @@ def test_get_access_token(self): assert data['refresh_token'] == 'foobar' +class TestRefreshTokenGenerator(OAuthSuite): + + def create_oauth_provider(self, app): + + def at_generator(request): + return 'foobar' + + def rt_generator(request): + return 'abracadabra' + + app.config['OAUTH2_PROVIDER_TOKEN_GENERATOR'] = at_generator + app.config['OAUTH2_PROVIDER_REFRESH_TOKEN_GENERATOR'] = rt_generator + return default_provider(app) + + def test_get_access_token(self): + rv = self.client.post(authorize_url, data={'confirm': 'yes'}) + rv = self.client.get(clean_url(http://github.com/rv.location)) + data = json.loads(u(rv.data)) + assert data['access_token'] == 'foobar' + assert data['refresh_token'] == 'abracadabra' + + class TestConfidentialClient(OAuthSuite): def create_oauth_provider(self, app): From 2885eab77ceea8633f98c6031576de01bacae2d9 Mon Sep 17 00:00:00 2001 From: Rex Posadas Date: Fri, 17 Oct 2014 08:05:58 -0500 Subject: [PATCH 128/279] correct spelling for authorization --- docs/oauth1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/oauth1.rst b/docs/oauth1.rst index 69fba61a..a1b9f082 100644 --- a/docs/oauth1.rst +++ b/docs/oauth1.rst @@ -2,7 +2,7 @@ OAuth1 Server ============= This part of documentation covers the tutorial of setting up an OAuth1 -provider. An OAuth1 server concerns how to grant the auothorization and +provider. An OAuth1 server concerns how to grant the authorization and how to protect the resource. Register an **OAuth** provider:: from flask_oauthlib.provider import OAuth1Provider From 0e8ec33df7b2aca636b963423f6d78f42b4334ee Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 24 Oct 2014 14:23:36 +0800 Subject: [PATCH 129/279] Always test oauthlib master. https://github.com/idan/oauthlib/issues/283 --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index e1b24487..c253bde6 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = py26,py27,py33,py34,pypy [testenv] deps = + git+https://github.com/idan/oauthlib.git#egg=oauthlib nose Mock Flask-SQLAlchemy From 2fe73b7fced73b4788bf4eeb0232bcf5b58dffd4 Mon Sep 17 00:00:00 2001 From: Matt Crampton Date: Tue, 23 Sep 2014 15:17:53 -0700 Subject: [PATCH 130/279] Fixing malformed HTML in the example templates --- example/templates/index.html | 6 +++++- example/templates/layout.html | 13 +++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/example/templates/index.html b/example/templates/index.html index 361d4655..48612ccc 100644 --- a/example/templates/index.html +++ b/example/templates/index.html @@ -4,17 +4,19 @@

Overview

{% if g.user %}

Hello {{ g.user.screen_name }}! Wanna tweet something? +

+

{% if tweets %}

Your Timeline

    {% for tweet in tweets %}
  • {{ tweet.user.screen_name }}: {{ tweet.text|urlize }} + }}">{{ tweet.user.screen_name }}: {{ tweet.text|urlize }}
  • {% endfor %}
{% endif %} @@ -22,8 +24,10 @@

Your Timeline

Sign in to view your public timeline and to tweet from this example application. +

sign in +

{% endif %} {% endblock %} diff --git a/example/templates/layout.html b/example/templates/layout.html index 6c9649d0..b27b3530 100644 --- a/example/templates/layout.html +++ b/example/templates/layout.html @@ -1,17 +1,22 @@ + {% block title %}Welcome{% endblock %} | Flask OAuth Example + +

Flask OAuth Example

{% for message in get_flashed_messages() %} -

{{ message }} +

{{ message }}

{% endfor %} {% block body %}{% endblock %} + + From 01465ce2a6f2512dba1b16e5010ac33c995eb578 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 14 Nov 2014 21:38:52 +0800 Subject: [PATCH 131/279] Add a convenient way to fetch oauth data in non vanilla Flask project. Related: https://github.com/lepture/flask-oauthlib/issues/154 --- flask_oauthlib/provider/oauth2.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 5bf114e6..b387a8de 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -462,6 +462,23 @@ def confirm_authorization_request(self): self.error_uri, {'error': 'unknown'} )) + def verify_request(self, scopes): + """Verify current request, get the oauth data. + + If you can't use the ``require_oauth`` decorator, you can fetch + the data in your request body:: + + def your_handler(): + valid, req = oauth.verify_request(['email']) + if valid: + return jsonify(user=req.user) + return jsonify(status='error') + """ + uri, http_method, body, headers = extract_params() + return self.server.verify_request( + uri, http_method, body, headers, scopes + ) + def token_handler(self, f): """Access/refresh token handler decorator. @@ -531,11 +548,7 @@ def decorated(*args, **kwargs): if hasattr(request, 'oauth') and request.oauth: return f(*args, **kwargs) - server = self.server - uri, http_method, body, headers = extract_params() - valid, req = server.verify_request( - uri, http_method, body, headers, scopes - ) + valid, req = self.verify_request(scopes) for func in self._after_request_funcs: valid, req = func(valid, req) From 8192036bbb43f35b7af5a5eca70d8662f7bd2d0c Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 3 Dec 2014 17:22:55 +0800 Subject: [PATCH 132/279] Version bump 0.8.0 --- docs/changelog.rst | 14 +++++++++++++- flask_oauthlib/__init__.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b89de5ce..367eaded 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,10 +5,22 @@ Changelog Here you can see the full list of changes between each Flask-OAuthlib release. +Version 0.8.0 +------------- + +Released on Dec 3, 2014 + +.. module:: flask_oauthlib.provider.oauth2 + +- New feature for generating refresh tokens +- Add new function :meth:`OAuth2Provider.verify_request` for non vanilla Flask projects +- Some small bugfixes + + Version 0.7.0 ------------- -Release date to be decided. +Released on Aug 20, 2014 .. module:: flask_oauthlib.client diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index 36dbfdcc..2b77ba12 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -11,7 +11,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = "0.7.0" +__version__ = "0.8.0" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' From a33df6d80b815bf7dfb985b0cd857947cb654a5c Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 16 Dec 2014 12:21:28 +0800 Subject: [PATCH 133/279] Remove password from logging for security. Issue https://github.com/lepture/flask-oauthlib/issues/166 reported by @zgoda-mobica --- flask_oauthlib/provider/oauth2.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index b387a8de..8863ee03 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -931,8 +931,7 @@ def validate_user(self, username, password, client, request, Attach user object on request for later using. """ - log.debug('Validating username %r and password %r', - username, password) + log.debug('Validating username %r and its password', username) if self._usergetter is not None: user = self._usergetter( username, password, client, request, *args, **kwargs From eb0f14c61f75c5c4749489bef913982310f1790b Mon Sep 17 00:00:00 2001 From: mdxs Date: Tue, 9 Dec 2014 09:10:05 +0100 Subject: [PATCH 134/279] Updated link in changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 367eaded..19122792 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -28,7 +28,7 @@ Released on Aug 20, 2014 :meth:`OAuthRemoteApp.authorized_response`. - Add revocation endpoint via `#131`_. - Handle unknown exceptions in providers. -- Add PATCH method for client via `134`_. +- Add PATCH method for client via `#134`_. .. _`#131`: https://github.com/lepture/flask-oauthlib/pull/131 .. _`#134`: https://github.com/lepture/flask-oauthlib/pull/134 From f051f001856496498e6741ce35ef0abd0dc1db1a Mon Sep 17 00:00:00 2001 From: Jarek Zgoda Date: Mon, 5 Jan 2015 10:48:30 +0100 Subject: [PATCH 135/279] Add appropriate headers when making POST request for access token --- flask_oauthlib/client.py | 1 + tests/oauth2/client.py | 2 +- tests/oauth2/server.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index a626cf9d..de0ac59f 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -625,6 +625,7 @@ def handle_oauth2_response(self): body = client.prepare_request_body(**remote_args) resp, content = self.http_request( self.expand_url(http://github.com/self.access_token_url), + headers={'Content-Type': 'application/x-www-form-urlencoded'}, data=to_bytes(body, self.encoding), method=self.access_token_method, ) diff --git a/tests/oauth2/client.py b/tests/oauth2/client.py index 904d9cd1..39cbb0bb 100644 --- a/tests/oauth2/client.py +++ b/tests/oauth2/client.py @@ -12,7 +12,7 @@ def create_client(app): request_token_params={'scope': 'email'}, base_url='http://127.0.0.1:5000/api/', request_token_url=None, - access_token_method='GET', + access_token_method='POST', access_token_url='http://127.0.0.1:5000/oauth/token', authorize_url='http://127.0.0.1:5000/oauth/authorize' ) diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index f1e689e6..b1b5b686 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -270,7 +270,7 @@ def authorize(*args, **kwargs): confirm = request.form.get('confirm', 'no') return confirm == 'yes' - @app.route('/oauth/token') + @app.route('/oauth/token', methods=['POST', 'GET']) @oauth.token_handler def access_token(): return {} From c8a58be94094af75f0356f630451a207b3cb9427 Mon Sep 17 00:00:00 2001 From: Minh Tran Date: Sat, 10 Jan 2015 01:36:47 +0000 Subject: [PATCH 136/279] Make OAuth2 authorize_handler support HTTP HEAD the same way as GET --- flask_oauthlib/provider/oauth2.py | 2 +- tests/oauth2/server.py | 9 ++++++++- tests/oauth2/test_oauth2.py | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 8863ee03..a69db840 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -379,7 +379,7 @@ def decorated(*args, **kwargs): server = self.server uri, http_method, body, headers = extract_params() - if request.method == 'GET': + if request.method in ('GET', 'HEAD'): redirect_uri = request.args.get('redirect_uri', None) log.debug('Found redirect_uri %s.', redirect_uri) try: diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index b1b5b686..a1d4cb0e 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -1,6 +1,6 @@ # coding: utf-8 from datetime import datetime, timedelta -from flask import g, render_template, request, jsonify +from flask import g, render_template, request, jsonify, make_response from flask.ext.sqlalchemy import SQLAlchemy from sqlalchemy.orm import relationship from flask_oauthlib.provider import OAuth2Provider @@ -267,6 +267,13 @@ def authorize(*args, **kwargs): # render a page for user to confirm the authorization return render_template('confirm.html') + if request.method == 'HEAD': + # if HEAD is supported properly, request parameters like + # client_id should be validated the same way as for 'GET' + response = make_response('', 200) + response.headers['X-Client-ID'] = kwargs.get('client_id') + return response + confirm = request.form.get('confirm', 'no') return confirm == 'yes' diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index abd10cc1..dbcb5809 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -94,6 +94,10 @@ def test_oauth_authorize_valid_url(http://github.com/self): assert 'code=' in rv.location assert 'state' in rv.location + def test_http_head_oauth_authorize_valid_url(http://github.com/self): + rv = self.client.head(authorize_url) + assert rv.headers['X-Client-ID'] == 'dev' + def test_get_access_token(self): rv = self.client.post(authorize_url, data={'confirm': 'yes'}) rv = self.client.get(clean_url(http://github.com/rv.location)) From bc86dff3db7fc53581a5e927eda8ae1e70be418c Mon Sep 17 00:00:00 2001 From: Minh Tran Date: Sat, 10 Jan 2015 01:36:47 +0000 Subject: [PATCH 137/279] Make OAuth2 authorize_handler support HTTP HEAD the same way as GET --- flask_oauthlib/provider/oauth2.py | 2 +- tests/oauth2/server.py | 9 ++++++++- tests/oauth2/test_oauth2.py | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 8863ee03..a69db840 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -379,7 +379,7 @@ def decorated(*args, **kwargs): server = self.server uri, http_method, body, headers = extract_params() - if request.method == 'GET': + if request.method in ('GET', 'HEAD'): redirect_uri = request.args.get('redirect_uri', None) log.debug('Found redirect_uri %s.', redirect_uri) try: diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index b1b5b686..a1d4cb0e 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -1,6 +1,6 @@ # coding: utf-8 from datetime import datetime, timedelta -from flask import g, render_template, request, jsonify +from flask import g, render_template, request, jsonify, make_response from flask.ext.sqlalchemy import SQLAlchemy from sqlalchemy.orm import relationship from flask_oauthlib.provider import OAuth2Provider @@ -267,6 +267,13 @@ def authorize(*args, **kwargs): # render a page for user to confirm the authorization return render_template('confirm.html') + if request.method == 'HEAD': + # if HEAD is supported properly, request parameters like + # client_id should be validated the same way as for 'GET' + response = make_response('', 200) + response.headers['X-Client-ID'] = kwargs.get('client_id') + return response + confirm = request.form.get('confirm', 'no') return confirm == 'yes' diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index abd10cc1..dbcb5809 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -94,6 +94,10 @@ def test_oauth_authorize_valid_url(http://github.com/self): assert 'code=' in rv.location assert 'state' in rv.location + def test_http_head_oauth_authorize_valid_url(http://github.com/self): + rv = self.client.head(authorize_url) + assert rv.headers['X-Client-ID'] == 'dev' + def test_get_access_token(self): rv = self.client.post(authorize_url, data={'confirm': 'yes'}) rv = self.client.get(clean_url(http://github.com/rv.location)) From e29cb202ee582bed67a21982daf087e6b8b5d5e4 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sat, 23 Aug 2014 23:22:52 +0800 Subject: [PATCH 138/279] ignore the vim swap files. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 36e9635c..976a959e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.pyc *.pyo *.egg-info +*.swp __pycache__ build develop-eggs From e0a03113cb9986bbebfb452b9c6a1fbb8f38070b Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sat, 23 Aug 2014 23:50:40 +0800 Subject: [PATCH 139/279] add new deps: requests-oauthlib --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 3abff62d..ac531d15 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ def fread(filename): install_requires=[ 'Flask', 'oauthlib>=0.6.2', + 'requests-oauthlib>=0.4.1', ], tests_require=['nose', 'Flask-SQLAlchemy', 'mock'], test_suite='nose.collector', From 298e448c90f997e3c947fe730b5f5b72661aa835 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 24 Aug 2014 00:01:05 +0800 Subject: [PATCH 140/279] implement the experiment client with requests-oauthlib as backend. implement the OAuthProperty descriptor. comment the oauth property's implementation. add factory method to get extension state from flask app. And remove the binding of extension state and the app. decouple remote apps from the ext state. remove config_prefix (and use app.name instead). add session class. fix up typo. implement the oauth 1.0a authorization. endpoint_url is optional. implement authorized_response method. wrap the access token response. extract the session factory into mixin class. move client module into its package. extract components into standalone modules. refine the module names. human friendly exception message. remove the mixin classes and use application base class. integrate oauth application with extension. refine the base application class. add example for OAuth 1.0a protocol. implement oauth 2 application with insecure_transport(debug) mode and compliance hooks. add example for OAuth 2 protocol. install deps in travis ci. use douban_compliance_fix in the example file. See also: https://github.com/requests/requests-oauthlib/pull/138 enable the insecure transport in testing mode too. warning for Man-in-the-middle attack in production environment. revise the warning message. --- example/contrib/experiment-client/.gitignore | 1 + example/contrib/experiment-client/douban.py | 60 ++++ example/contrib/experiment-client/twitter.py | 55 ++++ flask_oauthlib/contrib/client/__init__.py | 60 ++++ flask_oauthlib/contrib/client/application.py | 272 +++++++++++++++++++ flask_oauthlib/contrib/client/descriptor.py | 69 +++++ flask_oauthlib/contrib/client/exceptions.py | 9 + flask_oauthlib/contrib/client/structure.py | 17 ++ requirements.txt | 1 + 9 files changed, 544 insertions(+) create mode 100644 example/contrib/experiment-client/.gitignore create mode 100644 example/contrib/experiment-client/douban.py create mode 100644 example/contrib/experiment-client/twitter.py create mode 100644 flask_oauthlib/contrib/client/__init__.py create mode 100644 flask_oauthlib/contrib/client/application.py create mode 100644 flask_oauthlib/contrib/client/descriptor.py create mode 100644 flask_oauthlib/contrib/client/exceptions.py create mode 100644 flask_oauthlib/contrib/client/structure.py diff --git a/example/contrib/experiment-client/.gitignore b/example/contrib/experiment-client/.gitignore new file mode 100644 index 00000000..2c603613 --- /dev/null +++ b/example/contrib/experiment-client/.gitignore @@ -0,0 +1 @@ +dev.cfg diff --git a/example/contrib/experiment-client/douban.py b/example/contrib/experiment-client/douban.py new file mode 100644 index 00000000..4849f251 --- /dev/null +++ b/example/contrib/experiment-client/douban.py @@ -0,0 +1,60 @@ +from flask import Flask, url_for, session, jsonify +from flask.ext.oauthlib.contrib.client import OAuth + + +class AppConfig(object): + DEBUG = True + SECRET_KEY = 'your-secret-key' + DOUBAN_CLIENT_ID = 'your-api-key' + DOUBAN_CLIENT_SECRET = 'your-api-secret' + DOUBAN_SCOPE = [ + 'douban_basic_common', + 'shuo_basic_r', + ] + +app = Flask(__name__) +app.config.from_object(AppConfig) +app.config.from_pyfile('dev.cfg', silent=True) + +oauth = OAuth(app) +# see also https://github.com/requests/requests-oauthlib/pull/138 +douban = oauth.remote_app( + name='douban', + version='2', + endpoint_url='https://api.douban.com/', + access_token_url='https://www.douban.com/service/auth2/token', + authorization_url='https://www.douban.com/service/auth2/auth', + compliance_fixes='.douban:douban_compliance_fix') + + +@app.route('/') +def home(): + if oauth_douban_token(): + response = douban.get('v2/user/~me') + return jsonify(response=response.json()) + return 'Login' % url_for('oauth_douban') + + +@app.route('/auth/douban') +def oauth_douban(): + callback_uri = url_for('oauth_douban_callback', _external=True) + return douban.authorize(callback_uri) + + +@app.route('/auth/douban/callback') +def oauth_douban_callback(): + response = douban.authorized_response() + if response: + session['token'] = response + return repr(dict(response)) + else: + return 'T_T Denied' % (url_for('oauth_douban')) + + +@douban.tokengetter +def oauth_douban_token(): + return session.get('token') + + +if __name__ == '__main__': + app.run() diff --git a/example/contrib/experiment-client/twitter.py b/example/contrib/experiment-client/twitter.py new file mode 100644 index 00000000..15e5e000 --- /dev/null +++ b/example/contrib/experiment-client/twitter.py @@ -0,0 +1,55 @@ +from flask import Flask, url_for, session, jsonify +from flask.ext.oauthlib.contrib.client import OAuth + + +class DefaultConfig(object): + DEBUG = True + SECRET_KEY = 'your-secret-key' + TWITTER_CONSUMER_KEY = 'your-api-key' + TWITTER_CONSUMER_SECRET = 'your-api-secret' + +app = Flask(__name__) +app.config.from_object(DefaultConfig) +app.config.from_pyfile('dev.cfg', silent=True) + +oauth = OAuth(app) +twitter = oauth.remote_app( + name='twitter', + version='1', + endpoint_url='https://api.twitter.com/1.1/', + request_token_url='https://api.twitter.com/oauth/request_token', + access_token_url='https://api.twitter.com/oauth/access_token', + authorization_url='https://api.twitter.com/oauth/authorize') + + +@app.route('/') +def home(): + if oauth_twitter_token(): + response = twitter.get('statuses/home_timeline.json') + return jsonify(response=response.json()) + return 'Login' % url_for('oauth_twitter') + + +@app.route('/auth/twitter') +def oauth_twitter(): + callback_uri = url_for('oauth_twitter_callback', _external=True) + return twitter.authorize(callback_uri) + + +@app.route('/auth/twitter/callback') +def oauth_twitter_callback(): + response = twitter.authorized_response() + if response: + session['token'] = (response.token, response.token_secret) + return repr(dict(response)) + else: + return 'T_T Denied' % (url_for('oauth_twitter')) + + +@twitter.tokengetter +def oauth_twitter_token(): + return session.get('token') + + +if __name__ == '__main__': + app.run() diff --git a/flask_oauthlib/contrib/client/__init__.py b/flask_oauthlib/contrib/client/__init__.py new file mode 100644 index 00000000..27951c8d --- /dev/null +++ b/flask_oauthlib/contrib/client/__init__.py @@ -0,0 +1,60 @@ +import copy + +from .application import OAuth1Application, OAuth2Application + + +__all__ = ['OAuth', 'OAuth1Application', 'OAuth2Application'] + + +class OAuth(object): + """The extension to integrate OAuth 1.0a/2.0 to Flask applications.""" + + state_key = 'oauthlib.contrib.client' + + def __init__(self, app=None): + self.remote_apps = {} + if app is not None: + self.init_app(app) + + def init_app(self, app): + app.extensions = getattr(app, 'extensions', {}) + app.extensions[self.state_key] = self + + def add_remote_app(self, remote_app, name=None, **kwargs): + """Adds remote application and applies custom attributes on it. + + If the application instance's name is different from the argument + provided name, or the keyword arguments is not empty, then the + application instance will not be modified but be copied as a + prototype. + + :param remote_app: the remote application instance. + :type remote_app: the subclasses of :class:`BaseApplication` + :params kwargs: the overriding attributes for the application instance. + """ + if name is None: + name = remote_app.name + if name != remote_app.name or kwargs: + remote_app = copy.copy(remote_app) + remote_app.name = name + vars(remote_app).update(kwargs) + self.remote_apps[name] = remote_app + return remote_app + + def remote_app(self, name, version, **kwargs): + """Creates and adds new remote application. + + :param name: the remote application's name. + :param version: '1' or '2', the version code of OAuth protocol. + :param kwargs: the attributes of remote application. + """ + if version == '1': + remote_app = OAuth1Application(name) + elif version == '2': + remote_app = OAuth2Application(name) + else: + raise ValueError('unkonwn version %r' % version) + return self.add_remote_app(remote_app, **kwargs) + + def __getitem__(self, name): + return self.remote_apps[name] diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py new file mode 100644 index 00000000..cbf6d688 --- /dev/null +++ b/flask_oauthlib/contrib/client/application.py @@ -0,0 +1,272 @@ +""" + flask_oauthlib.contrib.client + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + An experiment client with requests-oauthlib as backend. +""" + +import os +import contextlib +import warnings +try: + from urllib.parse import urljoin +except ImportError: + from urlparse import urljoin + +from flask import current_app, redirect, request +from requests_oauthlib import OAuth1Session, OAuth2Session +from oauthlib.oauth2.rfc6749.errors import MissingCodeError +from werkzeug.utils import import_string + +from .descriptor import OAuthProperty, WebSessionData +from .structure import OAuth1Response, OAuth2Response +from .exceptions import AccessTokenNotFound + + +__all__ = ['OAuth1Application', 'OAuth2Application'] + + +class BaseApplication(object): + """The base class of OAuth application.""" + + session_class = None + + def __init__(self, name, **kwargs): + # oauth property required + self.name = name + # other descriptor assignable attributes + for k, v in kwargs.items(): + if not hasattr(self.__class__, k): + raise TypeError('descriptor %r not found' % k) + setattr(self, k, v) + + def __repr__(self): + class_name = self.__class__.__name__ + return '<%s:%s at %s>' % (class_name, self.name, hex(id(self))) + + def tokengetter(self, fn): + self._tokengetter = fn + return fn + + def obtain_token(self): + tokengetter = getattr(self, '_tokengetter', None) + if tokengetter is None: + raise RuntimeError('%r missing tokengetter' % self) + return tokengetter() + + @property + def client(self): + """The lazy-created OAuth session with the return value of + :meth:`tokengetter`. + + :returns: The OAuth session instance or ``None`` while token missing. + """ + raise NotImplementedError + + def authorize(self, callback_uri, code=302): + """Redirects to third-part URL and authorizes. + + :param callback_uri: the callback URI. if you generate it with the + :func:`~flask.url_for`, don't forget to use the + ``_external=True`` keyword argument. + :param code: default is 302. the HTTP code for redirection. + :returns: the redirection response. + """ + raise NotImplementedError + + def authorized_response(self): + """Obtains access token from third-part API. + + :returns: the response with the type of :class:`OAuthResponse` dict, + or ``None`` if the authorization has been denied. + """ + raise NotImplementedError + + forward_methods = frozenset([ + 'head', + 'get', + 'post', + 'put', + 'delete', + 'patch', + ]) + + # magic: generate methods which forward to self.client + def _make_method(_method_name): + def _method(self, url, *args, **kwargs): + url = urljoin(self.endpoint_url, url) + return getattr(self.client, _method_name)(url, *args, **kwargs) + return _method + for _method_name in forward_methods: + _method = _make_method(_method_name) + _method.func_name = _method.__name__ = _method_name + locals()[_method_name] = _method + del _make_method + del _method + del _method_name + + +class OAuth1Application(BaseApplication): + """The remote application for OAuth 1.0a.""" + + endpoint_url = OAuthProperty('endpoint_url', default='') + request_token_url = OAuthProperty('request_token_url') + access_token_url = OAuthProperty('access_token_url') + authorization_url = OAuthProperty('authorization_url') + + consumer_key = OAuthProperty('consumer_key') + consumer_secret = OAuthProperty('consumer_secret') + + session_class = OAuth1Session + + _session_request_token = WebSessionData('req_token') + + @property + def client(self): + token = self.obtain_token() + if token is None: + raise AccessTokenNotFound + access_token, access_token_secret = token + return self.make_oauth_session( + resource_owner_key=access_token, + resource_owner_secret=access_token_secret) + + def authorize(self, callback_uri, code=302): + oauth = self.make_oauth_session(callback_uri=callback_uri) + + # fetches request token + response = oauth.fetch_request_token(self.request_token_url) + request_token = response['oauth_token'] + request_token_secret = response['oauth_token_secret'] + + # stores request token and callback uri + self._session_request_token = (request_token, request_token_secret) + + # redirects to third-part URL + authorization_url = oauth.authorization_url(http://github.com/self.authorization_url) + return redirect(authorization_url, code) + + def authorized_response(self): + oauth = self.make_oauth_session() + + # obtains verifier + try: + response = oauth.parse_authorization_response(request.url) + except ValueError as e: + if 'denied' not in repr(e).split("'"): + raise + return # authorization denied + verifier = response['oauth_verifier'] + + # restores request token from session + if not self._session_request_token: + return + request_token, request_token_secret = self._session_request_token + del self._session_request_token + + # obtains access token + oauth = self.make_oauth_session( + resource_owner_key=request_token, + resource_owner_secret=request_token_secret, + verifier=verifier) + oauth_tokens = oauth.fetch_access_token(self.access_token_url) + return OAuth1Response(oauth_tokens) + + def make_oauth_session(self, **kwargs): + oauth = self.session_class( + self.consumer_key, client_secret=self.consumer_secret, **kwargs) + return oauth + + +class OAuth2Application(BaseApplication): + """The remote application for OAuth 2.""" + + session_class = OAuth2Session + + endpoint_url = OAuthProperty('endpoint_url', default='') + access_token_url = OAuthProperty('access_token_url') + authorization_url = OAuthProperty('authorization_url') + + client_id = OAuthProperty('client_id') + client_secret = OAuthProperty('client_secret') + scope = OAuthProperty('scope', default=None) + + compliance_fixes = OAuthProperty('compliance_fixes', default=None) + + _session_state = WebSessionData('state') + _session_redirect_url = WebSessionData('redir') + + @property + def client(self): + token = self.obtain_token() + if token is None: + raise AccessTokenNotFound + return self.session_class(self.client_id, token=token) + + def authorize(self, callback_uri, code=302, **kwargs): + oauth = self.make_oauth_session(redirect_uri=callback_uri) + authorization_url, state = oauth.authorization_url( + self.authorization_url, **kwargs) + self._session_state = state + self._session_redirect_url = callback_uri + return redirect(authorization_url, code) + + def authorized_response(self): + oauth = self.make_oauth_session( + state=self._session_state, + redirect_uri=self._session_redirect_url) + del self._session_state + del self._session_redirect_url + + with self.insecure_transport(): + try: + token = oauth.fetch_token( + self.access_token_url, client_secret=self.client_secret, + authorization_response=request.url) + except MissingCodeError: + return + + return OAuth2Response(token) + + def make_oauth_session(self, **kwargs): + # joins scope into unicode + kwargs.setdefault('scope', self.scope) + if kwargs['scope']: + kwargs['scope'] = u','.join(kwargs['scope']) + + # creates session + oauth = self.session_class(self.client_id, **kwargs) + + # patches session + compliance_fixes = self.compliance_fixes + if compliance_fixes.startswith('.'): + compliance_fixes = \ + 'requests_oauthlib.compliance_fixes' + compliance_fixes + apply_fixes = import_string(compliance_fixes) + oauth = apply_fixes(oauth) + + return oauth + + @contextlib.contextmanager + def insecure_transport(self): + """Creates a context to enable the oauthlib environment variable in + order to debug with insecure transport. + """ + origin = os.environ.get('OAUTHLIB_INSECURE_TRANSPORT') + if current_app.debug or current_app.testing: + try: + os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' + yield + finally: + if origin: + os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = origin + else: + os.environ.pop('OAUTHLIB_INSECURE_TRANSPORT', None) + else: + if origin: + warnings.warn( + 'OAUTHLIB_INSECURE_TRANSPORT has been found in os.environ ' + 'but the app is not running in debug mode or testing mode.' + ' It may put you in danger of the Man-in-the-middle attack' + ' while using OAuth 2.', RuntimeWarning) + yield diff --git a/flask_oauthlib/contrib/client/descriptor.py b/flask_oauthlib/contrib/client/descriptor.py new file mode 100644 index 00000000..e3658299 --- /dev/null +++ b/flask_oauthlib/contrib/client/descriptor.py @@ -0,0 +1,69 @@ +from flask import current_app, session + + +__all__ = ['OAuthProperty', 'WebSessionData'] + + +class OAuthProperty(object): + """The property which providing config item to remote applications. + + The application classes must have ``name`` to identity themselves. + """ + + _missing = object() + + def __init__(self, name, default=_missing): + self.name = name + self.default = default + + def __get__(self, instance, owner): + if instance is None: + return self + + # instance resources + instance_namespace = vars(instance) + instance_ident = instance.name + + # gets from instance namespace + if self.name in instance_namespace: + return instance_namespace[self.name] + + # gets from app config (or default value) + config_name = '{0}_{1}'.format(instance_ident, self.name).upper() + if config_name not in current_app.config: + if self.default is not self._missing: + return self.default + exception_message = ( + '{0!r} missing {1} \n\n You need to provide it in arguments' + ' `{0.__class__.__name__}(..., {1}="foobar", ...)` or in ' + 'app.config `{2}`').format(instance, self.name, config_name) + raise RuntimeError(exception_message) + return current_app.config[config_name] + + def __set__(self, instance, value): + # assigns into instance namespace + instance_namespace = vars(instance) + instance_namespace[self.name] = value + + +class WebSessionData(object): + """The property which providing accessing of Flask session.""" + + key_format = '_oauth_{0}_{1}' + + def __init__(self, ident): + self.ident = ident + + def make_key(self, instance): + return self.key_format.format(instance.name, self.ident) + + def __get__(self, instance, owner): + if instance is None: + return self + return session.get(self.make_key(instance)) + + def __set__(self, instance, value): + session[self.make_key(instance)] = value + + def __delete__(self, instance): + session.pop(self.make_key(instance), None) diff --git a/flask_oauthlib/contrib/client/exceptions.py b/flask_oauthlib/contrib/client/exceptions.py new file mode 100644 index 00000000..79487284 --- /dev/null +++ b/flask_oauthlib/contrib/client/exceptions.py @@ -0,0 +1,9 @@ +__all__ = ['OAuthException', 'AccessTokenNotFound'] + + +class OAuthException(Exception): + pass + + +class AccessTokenNotFound(OAuthException): + pass diff --git a/flask_oauthlib/contrib/client/structure.py b/flask_oauthlib/contrib/client/structure.py new file mode 100644 index 00000000..65f75e69 --- /dev/null +++ b/flask_oauthlib/contrib/client/structure.py @@ -0,0 +1,17 @@ +import operator + + +__all__ = ['OAuth1Response', 'OAuth2Response'] + + +class OAuth1Response(dict): + token = property(operator.itemgetter('oauth_token')) + token_secret = property(operator.itemgetter('oauth_token_secret')) + + +class OAuth2Response(dict): + access_token = property(operator.itemgetter('access_token')) + refresh_token = property(operator.itemgetter('refresh_token')) + token_type = property(operator.itemgetter('token_type')) + expires_in = property(operator.itemgetter('expires_in')) + expires_at = property(operator.itemgetter('expires_at')) diff --git a/requirements.txt b/requirements.txt index da38f09d..5fc83230 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ Flask Mock oauthlib +requests-oauthlib Flask-SQLAlchemy From 357087c6eb4b6ff7db14a7f854170501597de9d3 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 28 Dec 2014 15:48:08 +0800 Subject: [PATCH 141/279] add support for automatic token refresh. --- example/contrib/experiment-client/douban.py | 12 +++++++--- flask_oauthlib/contrib/client/application.py | 24 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/example/contrib/experiment-client/douban.py b/example/contrib/experiment-client/douban.py index 4849f251..3d6a6349 100644 --- a/example/contrib/experiment-client/douban.py +++ b/example/contrib/experiment-client/douban.py @@ -23,13 +23,14 @@ class AppConfig(object): version='2', endpoint_url='https://api.douban.com/', access_token_url='https://www.douban.com/service/auth2/token', + refresh_token_url='https://www.douban.com/service/auth2/token', authorization_url='https://www.douban.com/service/auth2/auth', compliance_fixes='.douban:douban_compliance_fix') @app.route('/') def home(): - if oauth_douban_token(): + if obtain_douban_token(): response = douban.get('v2/user/~me') return jsonify(response=response.json()) return 'Login' % url_for('oauth_douban') @@ -45,16 +46,21 @@ def oauth_douban(): def oauth_douban_callback(): response = douban.authorized_response() if response: - session['token'] = response + store_douban_token(response) return repr(dict(response)) else: return 'T_T Denied' % (url_for('oauth_douban')) @douban.tokengetter -def oauth_douban_token(): +def obtain_douban_token(): return session.get('token') +@douban.tokensaver +def store_douban_token(token): + session['token'] = token + + if __name__ == '__main__': app.run() diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py index cbf6d688..976bcca0 100644 --- a/flask_oauthlib/contrib/client/application.py +++ b/flask_oauthlib/contrib/client/application.py @@ -186,6 +186,7 @@ class OAuth2Application(BaseApplication): endpoint_url = OAuthProperty('endpoint_url', default='') access_token_url = OAuthProperty('access_token_url') authorization_url = OAuthProperty('authorization_url') + refresh_token_url = OAuthProperty('refresh_token_url', default='') client_id = OAuthProperty('client_id') client_secret = OAuthProperty('client_secret') @@ -203,6 +204,18 @@ def client(self): raise AccessTokenNotFound return self.session_class(self.client_id, token=token) + def tokensaver(self, fn): + """A decorator to register a callback function for saving refreshed + token while the old token has expired and the ``refresh_token_url`` has + been specified. + + It is necessary for using the automatic refresh mechanism. + + :param fn: the callback function with ``token`` as its unique argument. + """ + self._tokensaver = fn + return fn + def authorize(self, callback_uri, code=302, **kwargs): oauth = self.make_oauth_session(redirect_uri=callback_uri) authorization_url, state = oauth.authorization_url( @@ -234,6 +247,17 @@ def make_oauth_session(self, **kwargs): if kwargs['scope']: kwargs['scope'] = u','.join(kwargs['scope']) + # configures automatic token refresh if possible + if self.refresh_token_url: + if not hasattr(self, '_tokensaver'): + raise RuntimeError('missing tokensaver') + kwargs.setdefault('auto_refresh_url', self.refresh_token_url) + kwargs.setdefault('auto_refresh_kwargs', { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + }) + kwargs.setdefault('token_updater', self._tokensaver) + # creates session oauth = self.session_class(self.client_id, **kwargs) From 85da337e272daab4fac3ac589bf15612beb7f835 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 28 Dec 2014 16:20:15 +0800 Subject: [PATCH 142/279] document the extension class. --- flask_oauthlib/contrib/client/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/contrib/client/__init__.py b/flask_oauthlib/contrib/client/__init__.py index 27951c8d..0451c22e 100644 --- a/flask_oauthlib/contrib/client/__init__.py +++ b/flask_oauthlib/contrib/client/__init__.py @@ -7,7 +7,15 @@ class OAuth(object): - """The extension to integrate OAuth 1.0a/2.0 to Flask applications.""" + """The extension to integrate OAuth 1.0a/2.0 to Flask applications. + + oauth = OAuth(app) + + or:: + + oauth = OAuth() + oauth.init_app(app) + """ state_key = 'oauthlib.contrib.client' From cc26ecec7b26217c521711d92c640a93419c2f15 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sat, 10 Jan 2015 22:04:21 +0800 Subject: [PATCH 143/279] Guess version by parameters for OAuth client --- flask_oauthlib/contrib/client/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/contrib/client/__init__.py b/flask_oauthlib/contrib/client/__init__.py index 0451c22e..2eecb2da 100644 --- a/flask_oauthlib/contrib/client/__init__.py +++ b/flask_oauthlib/contrib/client/__init__.py @@ -49,13 +49,18 @@ def add_remote_app(self, remote_app, name=None, **kwargs): self.remote_apps[name] = remote_app return remote_app - def remote_app(self, name, version, **kwargs): + def remote_app(self, name, version=None, **kwargs): """Creates and adds new remote application. :param name: the remote application's name. :param version: '1' or '2', the version code of OAuth protocol. :param kwargs: the attributes of remote application. """ + if version is None: + if 'request_token_url' in kwargs: + version = '1' + else: + version = '2' if version == '1': remote_app = OAuth1Application(name) elif version == '2': From 909d92112ed30e7ff40f18b03a84d8b199bdd12e Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 19 Jan 2015 10:01:17 +0800 Subject: [PATCH 144/279] Add contrib client package --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ac531d15..e1afc16d 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ def fread(filename): "flask_oauthlib", "flask_oauthlib.provider", "flask_oauthlib.contrib", + "flask_oauthlib.contrib.client", ], description="OAuthlib for Flask", zip_safe=False, From 1a826b7df491cb3acc4b92eca00432c776661505 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 19 Jan 2015 10:17:25 +0800 Subject: [PATCH 145/279] Contrib client API should be similar with original client --- flask_oauthlib/client.py | 3 ++- flask_oauthlib/contrib/client/__init__.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index de0ac59f..cae5f04a 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -38,6 +38,7 @@ class OAuth(object): oauth = OAuth(app) """ + state_key = 'oauthlib.contrib.client' def __init__(self, app=None): self.remote_apps = {} @@ -56,7 +57,7 @@ def init_app(self, app): """ self.app = app app.extensions = getattr(app, 'extensions', {}) - app.extensions['oauthlib.client'] = self + app.extensions[self.state_key] = self def remote_app(self, name, register=True, **kwargs): """Registers a new remote application. diff --git a/flask_oauthlib/contrib/client/__init__.py b/flask_oauthlib/contrib/client/__init__.py index 2eecb2da..281dd5e9 100644 --- a/flask_oauthlib/contrib/client/__init__.py +++ b/flask_oauthlib/contrib/client/__init__.py @@ -71,3 +71,12 @@ def remote_app(self, name, version=None, **kwargs): def __getitem__(self, name): return self.remote_apps[name] + + def __getattr__(self, key): + try: + return object.__getattribute__(self, key) + except AttributeError: + app = self.remote_apps.get(key) + if app: + return app + raise AttributeError('No such app: %s' % key) From 477b9c096aeb85eaeb609f6c361ea12fee16d8b0 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 19 Jan 2015 10:23:36 +0800 Subject: [PATCH 146/279] Fix state_key of OAuth --- flask_oauthlib/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index cae5f04a..100c2740 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -38,7 +38,7 @@ class OAuth(object): oauth = OAuth(app) """ - state_key = 'oauthlib.contrib.client' + state_key = 'oauthlib.client' def __init__(self, app=None): self.remote_apps = {} From ff25f9d9936fd2ddacd23313dcb9afe4f4081a21 Mon Sep 17 00:00:00 2001 From: digwtx Date: Mon, 19 Jan 2015 16:15:57 +0800 Subject: [PATCH 147/279] add QQ example --- example/qq.py | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 example/qq.py diff --git a/example/qq.py b/example/qq.py new file mode 100644 index 00000000..41b8005e --- /dev/null +++ b/example/qq.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +import os +import json +from flask import Flask, redirect, url_for, session, request, jsonify, Markup +from flask_oauthlib.client import OAuth + +QQ_APP_ID = os.getenv('QQ_APP_ID', '101187283') +QQ_APP_KEY = os.getenv('QQ_APP_KEY', '993983549da49e384d03adfead8b2489') + +app = Flask(__name__) +app.debug = True +app.secret_key = 'development' +oauth = OAuth(app) + +qq = oauth.remote_app( + 'qq', + consumer_key=QQ_APP_ID, + consumer_secret=QQ_APP_KEY, + base_url='https://graph.qq.com/', + request_token_url=None, + request_token_params={'scope': 'get_user_info'}, + access_token_url='/oauth2.0/token', + authorize_url='/oauth2.0/authorize', +) + + +def json_to_dict(x): + '''OAuthResponse class can't not parse the JSON data with content-type + text/html, so we need reload the JSON data manually''' + if x.find('callback') > -1: + pos_lb = x.find('{') + pos_rb = x.find('}') + x = x[pos_lb:pos_rb + 1] + try: + return json.loads(x, encoding='utf-8') + except: + return x + + +def update_qq_api_request_data(data={}): + '''Update some required parameters for OAuth2.0 API calls''' + defaults = { + 'openid': session.get('qq_openid'), + 'access_token': session.get('qq_token')[0], + 'oauth_consumer_key': QQ_APP_ID, + } + defaults.update(data) + return defaults + + +@app.route('/') +def index(): + '''just for verify website owner here.''' + return Markup('''''') + + +@app.route('/user_info') +def get_user_info(): + if 'qq_token' in session: + data = update_qq_api_request_data() + resp = qq.get('/user/get_user_info', data=data) + return jsonify(status=resp.status, data=resp.data) + return redirect(url_for('login')) + + +@app.route('/login') +def login(): + return qq.authorize(callback=url_for('authorized', _external=True)) + + +@app.route('/logout') +def logout(): + session.pop('qq_token', None) + return redirect(url_for('get_user_info')) + + +@app.route('/login/authorized') +def authorized(): + resp = qq.authorized_response() + if resp is None: + return 'Access denied: reason=%s error=%s' % ( + request.args['error_reason'], + request.args['error_description'] + ) + session['qq_token'] = (resp['access_token'], '') + + # Get openid via access_token, openid and access_token are needed for API calls + resp = qq.get('/oauth2.0/me', {'access_token': session['qq_token'][0]}) + resp = json_to_dict(resp.data) + if isinstance(resp, dict): + session['qq_openid'] = resp.get('openid') + + return redirect(url_for('get_user_info')) + + +@qq.tokengetter +def get_qq_oauth_token(): + return session.get('qq_token') + + +def change_qq_header(uri, headers, body): + '''QQ API don't need the authorization header, it will make a Bad Request''' + if 'Authorization' in headers: + headers.pop('Authorization', None) + return uri, headers, body + +qq.pre_request = change_qq_header + + +if __name__ == '__main__': + app.run() From a9107514475b49fc88ceae86bd63337488df1e7c Mon Sep 17 00:00:00 2001 From: digwtx Date: Wed, 21 Jan 2015 13:41:41 +0800 Subject: [PATCH 148/279] fix request headers issue in QQ example --- example/qq.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/example/qq.py b/example/qq.py index 41b8005e..a0afd119 100644 --- a/example/qq.py +++ b/example/qq.py @@ -99,10 +99,19 @@ def get_qq_oauth_token(): return session.get('qq_token') +def convert_keys_to_string(dictionary): + '''Recursively converts dictionary keys to strings.''' + if not isinstance(dictionary, dict): + return dictionary + return dict((str(k), convert_keys_to_string(v)) for k, v in dictionary.items()) + + def change_qq_header(uri, headers, body): - '''QQ API don't need the authorization header, it will make a Bad Request''' - if 'Authorization' in headers: - headers.pop('Authorization', None) + '''On SAE platform, when headers' keys are unicode type, will raise + ``HTTP Error 400: Bad request``, so need convert keys from unicode to str. + Otherwise, ignored it.''' + # uncomment below line while deploy on SAE platform + # headers = convert_keys_to_string(headers) return uri, headers, body qq.pre_request = change_qq_header From b6745c0d4bf09bb4e214802265d17ec635f93419 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sat, 24 Jan 2015 16:38:56 +0800 Subject: [PATCH 149/279] modify _method_name in factory function. --- flask_oauthlib/contrib/client/application.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py index 976bcca0..4f93912a 100644 --- a/flask_oauthlib/contrib/client/application.py +++ b/flask_oauthlib/contrib/client/application.py @@ -96,13 +96,11 @@ def _make_method(_method_name): def _method(self, url, *args, **kwargs): url = urljoin(self.endpoint_url, url) return getattr(self.client, _method_name)(url, *args, **kwargs) + _method.func_name = _method.__name__ = _method_name return _method for _method_name in forward_methods: - _method = _make_method(_method_name) - _method.func_name = _method.__name__ = _method_name - locals()[_method_name] = _method + locals()[_method_name] = _make_method(_method_name) del _make_method - del _method del _method_name From b650722c6182463ec5cd98b962e96eab37c8386d Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sat, 24 Jan 2015 16:50:34 +0800 Subject: [PATCH 150/279] add "make_client" method to allow sending request with specific token. --- flask_oauthlib/contrib/client/application.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py index 4f93912a..6f2764fc 100644 --- a/flask_oauthlib/contrib/client/application.py +++ b/flask_oauthlib/contrib/client/application.py @@ -124,6 +124,16 @@ def client(self): token = self.obtain_token() if token is None: raise AccessTokenNotFound + return self.make_client(token) + + def make_client(self, token): + """Creates a client with specific access token pair. + + :param token: a tuple of access token pair: + ``(access_token, access_token_secret)``. + :returns: a :class:`requests_oauthlib.oauth1_session.OAuth1Session` + object. + """ access_token, access_token_secret = token return self.make_oauth_session( resource_owner_key=access_token, @@ -200,6 +210,15 @@ def client(self): token = self.obtain_token() if token is None: raise AccessTokenNotFound + return self.make_client(token) + + def make_client(self, token): + """Creates a client with specific access token dictionary. + + :param token: a dictionary of access token response. + :returns: a :class:`requests_oauthlib.oauth2_session.OAuth2Session` + object. + """ return self.session_class(self.client_id, token=token) def tokensaver(self, fn): From e81decc87e6d2f18da08d88a24c23bb6db872ede Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sat, 24 Jan 2015 17:40:50 +0800 Subject: [PATCH 151/279] cache clients for the same application and access token. --- flask_oauthlib/contrib/client/__init__.py | 28 ++++- flask_oauthlib/contrib/client/application.py | 107 +++++++++++++------ 2 files changed, 100 insertions(+), 35 deletions(-) diff --git a/flask_oauthlib/contrib/client/__init__.py b/flask_oauthlib/contrib/client/__init__.py index 281dd5e9..4b777b44 100644 --- a/flask_oauthlib/contrib/client/__init__.py +++ b/flask_oauthlib/contrib/client/__init__.py @@ -1,5 +1,8 @@ import copy +from flask import current_app +from werkzeug.local import LocalProxy + from .application import OAuth1Application, OAuth2Application @@ -26,7 +29,7 @@ def __init__(self, app=None): def init_app(self, app): app.extensions = getattr(app, 'extensions', {}) - app.extensions[self.state_key] = self + app.extensions[self.state_key] = OAuthState() def add_remote_app(self, remote_app, name=None, **kwargs): """Adds remote application and applies custom attributes on it. @@ -46,6 +49,8 @@ def add_remote_app(self, remote_app, name=None, **kwargs): remote_app = copy.copy(remote_app) remote_app.name = name vars(remote_app).update(kwargs) + if not hasattr(remote_app, 'clients'): + remote_app.clients = cached_clients self.remote_apps[name] = remote_app return remote_app @@ -62,9 +67,9 @@ def remote_app(self, name, version=None, **kwargs): else: version = '2' if version == '1': - remote_app = OAuth1Application(name) + remote_app = OAuth1Application(name, clients=cached_clients) elif version == '2': - remote_app = OAuth2Application(name) + remote_app = OAuth2Application(name, clients=cached_clients) else: raise ValueError('unkonwn version %r' % version) return self.add_remote_app(remote_app, **kwargs) @@ -80,3 +85,20 @@ def __getattr__(self, key): if app: return app raise AttributeError('No such app: %s' % key) + + +class OAuthState(object): + + def __init__(self): + self.cached_clients = {} + + +def get_cached_clients(): + """Gets the cached clients dictionary in current context.""" + if OAuth.state_key not in current_app.extensions: + raise RuntimeError('%r is not initialized.' % current_app) + state = current_app.extensions[OAuth.state_key] + return state.cached_clients + + +cached_clients = LocalProxy(get_cached_clients) diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py index 6f2764fc..437d15b7 100644 --- a/flask_oauthlib/contrib/client/application.py +++ b/flask_oauthlib/contrib/client/application.py @@ -27,13 +27,25 @@ class BaseApplication(object): - """The base class of OAuth application.""" + """The base class of OAuth application. + + An application instance could be used in mupltiple context. It never stores + any session-scope state in the ``__dict__`` of itself. + + :param name: the name of this application. + :param clients: optional. a reference to the cached clients dictionary. + """ session_class = None + endpoint_url = OAuthProperty('endpoint_url', default='') - def __init__(self, name, **kwargs): + def __init__(self, name, clients=None, **kwargs): # oauth property required self.name = name + + if clients: + self.clients = clients + # other descriptor assignable attributes for k, v in kwargs.items(): if not hasattr(self.__class__, k): @@ -49,6 +61,11 @@ def tokengetter(self, fn): return fn def obtain_token(self): + """Obtains the access token by calling ``tokengetter`` which was + defined by users. + + :returns: token or ``None``. + """ tokengetter = getattr(self, '_tokengetter', None) if tokengetter is None: raise RuntimeError('%r missing tokengetter' % self) @@ -61,7 +78,24 @@ def client(self): :returns: The OAuth session instance or ``None`` while token missing. """ - raise NotImplementedError + token = self.obtain_token() + if token is None: + raise AccessTokenNotFound + return self._make_client_with_token() + + def _make_client_with_token(self, token): + """Uses cached client or create new one with specific token.""" + cached_clients = getattr(self, 'clients', None) + hashed_token = _hash_token(self, token) + + if cached_clients and hashed_token in cached_clients: + return cached_clients[hashed_token] + + client = self.make_client(token) # implemented in subclasses + if cached_clients: + cached_clients[hashed_token] = client + + return client def authorize(self, callback_uri, code=302): """Redirects to third-part URL and authorizes. @@ -91,23 +125,29 @@ def authorized_response(self): 'patch', ]) + def request(self, method, url, token=None, *args, **kwargs): + if token is None: + client = self.client + else: + client = self._make_client_with_token(token) + url = urljoin(self.endpoint_url, url) + return getattr(client, method)(url, *args, **kwargs) + # magic: generate methods which forward to self.client - def _make_method(_method_name): - def _method(self, url, *args, **kwargs): - url = urljoin(self.endpoint_url, url) - return getattr(self.client, _method_name)(url, *args, **kwargs) - _method.func_name = _method.__name__ = _method_name - return _method - for _method_name in forward_methods: - locals()[_method_name] = _make_method(_method_name) - del _make_method - del _method_name + def make_request_shortcut(method): + def shortcut(self, url, token=None, *args, **kwargs): + return self.request(method, url, token, *args, **kwargs) + shortcut.func_name = shortcut.__name__ = method + return shortcut + for method in forward_methods: + locals()[method] = make_request_shortcut(method) + del method + del make_request_shortcut class OAuth1Application(BaseApplication): """The remote application for OAuth 1.0a.""" - endpoint_url = OAuthProperty('endpoint_url', default='') request_token_url = OAuthProperty('request_token_url') access_token_url = OAuthProperty('access_token_url') authorization_url = OAuthProperty('authorization_url') @@ -119,22 +159,19 @@ class OAuth1Application(BaseApplication): _session_request_token = WebSessionData('req_token') - @property - def client(self): - token = self.obtain_token() - if token is None: - raise AccessTokenNotFound - return self.make_client(token) - def make_client(self, token): """Creates a client with specific access token pair. - :param token: a tuple of access token pair: - ``(access_token, access_token_secret)``. + :param token: a tuple of access token pair ``(token, token_secret)`` + or a dictionary of access token response. :returns: a :class:`requests_oauthlib.oauth1_session.OAuth1Session` object. """ - access_token, access_token_secret = token + if isinstance(token, dict): + access_token = token['token'] + access_token_secret = token['token_secret'] + else: + access_token, access_token_secret = token return self.make_oauth_session( resource_owner_key=access_token, resource_owner_secret=access_token_secret) @@ -191,7 +228,6 @@ class OAuth2Application(BaseApplication): session_class = OAuth2Session - endpoint_url = OAuthProperty('endpoint_url', default='') access_token_url = OAuthProperty('access_token_url') authorization_url = OAuthProperty('authorization_url') refresh_token_url = OAuthProperty('refresh_token_url', default='') @@ -205,13 +241,6 @@ class OAuth2Application(BaseApplication): _session_state = WebSessionData('state') _session_redirect_url = WebSessionData('redir') - @property - def client(self): - token = self.obtain_token() - if token is None: - raise AccessTokenNotFound - return self.make_client(token) - def make_client(self, token): """Creates a client with specific access token dictionary. @@ -311,3 +340,17 @@ def insecure_transport(self): ' It may put you in danger of the Man-in-the-middle attack' ' while using OAuth 2.', RuntimeWarning) yield + + +def _hash_token(application, token): + """Creates a hashable object for given token then we could use it as a + dictionary key. + """ + if isinstance(token, dict): + hashed_token = tuple(sorted(token.items())) + elif isinstance(token, tuple): + hashed_token = token + else: + raise TypeError('%r is unknown type of token' % token) + + return (application.__class__.__name__, application.name, hashed_token) From 280661db1453e51cc9e4a34778b1fdcb3dc01ed4 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 25 Jan 2015 15:23:19 +0800 Subject: [PATCH 152/279] implement all shortcuts of http methods without magic. --- flask_oauthlib/contrib/client/application.py | 28 +++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py index 437d15b7..b7530a23 100644 --- a/flask_oauthlib/contrib/client/application.py +++ b/flask_oauthlib/contrib/client/application.py @@ -8,6 +8,7 @@ import os import contextlib import warnings +import functools try: from urllib.parse import urljoin except ImportError: @@ -133,16 +134,23 @@ def request(self, method, url, token=None, *args, **kwargs): url = urljoin(self.endpoint_url, url) return getattr(client, method)(url, *args, **kwargs) - # magic: generate methods which forward to self.client - def make_request_shortcut(method): - def shortcut(self, url, token=None, *args, **kwargs): - return self.request(method, url, token, *args, **kwargs) - shortcut.func_name = shortcut.__name__ = method - return shortcut - for method in forward_methods: - locals()[method] = make_request_shortcut(method) - del method - del make_request_shortcut + def head(self, *args, **kwargs): + return self.request('head', *args, **kwargs) + + def get(self, *args, **kwargs): + return self.request('get', *args, **kwargs) + + def post(self, *args, **kwargs): + return self.request('post', *args, **kwargs) + + def put(self, *args, **kwargs): + return self.request('put', *args, **kwargs) + + def delete(self, *args, **kwargs): + return self.request('delete', *args, **kwargs) + + def patch(self, *args, **kwargs): + return self.request('patch', *args, **kwargs) class OAuth1Application(BaseApplication): From b1ad69e87657826e0d040ca817e3912b5d111ae8 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 29 Jan 2015 15:13:31 +0800 Subject: [PATCH 153/279] Remove exceptions for easy catching --- flask_oauthlib/provider/oauth2.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index a69db840..5cdc1452 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -395,11 +395,6 @@ def decorated(*args, **kwargs): except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e) return redirect(e.in_uri(redirect_uri or self.error_uri)) - except Exception as e: - log.warn('Exception: %r', e) - return redirect(add_params_to_uri( - self.error_uri, {'error': 'unknown'} - )) else: redirect_uri = request.values.get('redirect_uri', None) @@ -412,11 +407,6 @@ def decorated(*args, **kwargs): except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e) return redirect(e.in_uri(redirect_uri or self.error_uri)) - except Exception as e: - log.warn('Exception: %r', e) - return redirect(add_params_to_uri( - self.error_uri, {'error': 'unknown'} - )) if not isinstance(rv, bool): # if is a response or redirect @@ -456,11 +446,6 @@ def confirm_authorization_request(self): except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e) return redirect(e.in_uri(redirect_uri or self.error_uri)) - except Exception as e: - log.warn('Exception: %r', e) - return redirect(add_params_to_uri( - self.error_uri, {'error': 'unknown'} - )) def verify_request(self, scopes): """Verify current request, get the oauth data. From a70f66cecaaa1359c136b7e2c40af7f0fe831825 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 29 Jan 2015 21:49:39 +0800 Subject: [PATCH 154/279] Bind sqlalchemy for implicit grant --- flask_oauthlib/contrib/oauth2.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/flask_oauthlib/contrib/oauth2.py b/flask_oauthlib/contrib/oauth2.py index ec908fcd..715dc93e 100644 --- a/flask_oauthlib/contrib/oauth2.py +++ b/flask_oauthlib/contrib/oauth2.py @@ -175,7 +175,7 @@ def set_token(token, request, *args, **kwargs): provider.clientgetter(client_binding.get) if token: - token_binding = TokenBinding(token, session) + token_binding = TokenBinding(token, session, current_user) provider.tokengetter(token_binding.get) provider.tokensetter(token_binding.set) @@ -240,6 +240,9 @@ class TokenBinding(BaseBinding): """Object use by SQLAlchemyBinding to register the token getter and setter """ + def __init__(self, model, session, current_user=None): + self.current_user = current_user + super(TokenBinding, self).__init__(model, session) def get(self, access_token=None, refresh_token=None): """returns a Token object with the given access token or refresh token @@ -260,8 +263,17 @@ def set(self, token, request, *args, **kwargs): :param token: token object :param request: OAuthlib request object """ - tokens = self.query.filter_by(client_id=request.client.client_id, - user_id=request.user.id).all() + if hasattr(request, 'user') and request.user: + user = request.user + elif self.current_user: + # for implicit token + user = self.current_user() + + client = request.client + + tokens = self.query.filter_by( + client_id=client.client_id, + user_id=user.id).all() if tokens: for tk in tokens: self.session.delete(tk) @@ -272,8 +284,8 @@ def set(self, token, request, *args, **kwargs): tok = self.model(**token) tok.expires = expires - tok.client_id = request.client.client_id - tok.user_id = request.user.id + tok.client_id = client.client_id + tok.user_id = user.id self.session.add(tok) self.session.commit() From f84a4ef73f230459cb949da2a687527ee4eef330 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 30 Jan 2015 15:31:26 +0800 Subject: [PATCH 155/279] Redirect for safety --- flask_oauthlib/provider/oauth2.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 5cdc1452..ecd3d088 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -18,7 +18,7 @@ from werkzeug.utils import import_string from oauthlib import oauth2 from oauthlib.oauth2 import RequestValidator, Server -from oauthlib.common import to_unicode, add_params_to_uri +from oauthlib.common import to_unicode from ..utils import extract_params, decode_base64, create_response __all__ = ('OAuth2Provider', 'OAuth2RequestValidator') @@ -380,7 +380,7 @@ def decorated(*args, **kwargs): uri, http_method, body, headers = extract_params() if request.method in ('GET', 'HEAD'): - redirect_uri = request.args.get('redirect_uri', None) + redirect_uri = request.args.get('redirect_uri', self.error_uri) log.debug('Found redirect_uri %s.', redirect_uri) try: ret = server.validate_authorization_request( @@ -394,10 +394,12 @@ def decorated(*args, **kwargs): return redirect(e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e) - return redirect(e.in_uri(redirect_uri or self.error_uri)) + return redirect(e.in_uri(redirect_uri)) else: - redirect_uri = request.values.get('redirect_uri', None) + redirect_uri = request.values.get( + 'redirect_uri', self.error_uri + ) try: rv = f(*args, **kwargs) @@ -406,7 +408,7 @@ def decorated(*args, **kwargs): return redirect(e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e) - return redirect(e.in_uri(redirect_uri or self.error_uri)) + return redirect(e.in_uri(redirect_uri)) if not isinstance(rv, bool): # if is a response or redirect From aa3fa47d50bcf4e98f4c5db939a9391e684986f6 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 30 Jan 2015 17:13:39 +0800 Subject: [PATCH 156/279] Remove exceptions for easy catching in OAuth 1 provider --- flask_oauthlib/provider/oauth1.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index 3cdb26d0..7886a6be 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -408,11 +408,6 @@ def decorated(*args, **kwargs): return redirect(e.in_uri(self.error_uri)) except errors.InvalidClientError as e: return redirect(e.in_uri(self.error_uri)) - except Exception as e: - log.warn('Exception: %r', e) - return redirect(add_params_to_uri( - self.error_uri, {'error': 'unknown'} - )) return decorated def confirm_authorization_request(self): @@ -433,11 +428,6 @@ def confirm_authorization_request(self): return redirect(e.in_uri(self.error_uri)) except errors.InvalidClientError as e: return redirect(e.in_uri(self.error_uri)) - except Exception as e: - log.warn('Exception: %r', e) - return redirect(add_params_to_uri( - self.error_uri, {'error': 'unknown'} - )) def request_token_handler(self, f): """Request token handler decorator. @@ -464,11 +454,6 @@ def decorated(*args, **kwargs): return create_response(*ret) except errors.OAuth1Error as e: return _error_response(e) - except Exception as e: - log.warn('Exception: %r', e) - e.urlencoded = urlencode([('error', 'unknown')]) - e.status_code = 400 - return _error_response(e) return decorated def access_token_handler(self, f): @@ -496,11 +481,6 @@ def decorated(*args, **kwargs): return create_response(*ret) except errors.OAuth1Error as e: return _error_response(e) - except Exception as e: - log.warn('Exception: %r', e) - e.urlencoded = urlencode([('error', 'unknown')]) - e.status_code = 400 - return _error_response(e) return decorated def require_oauth(self, *realms, **kwargs): From ba53f778dbee47900c97c3e9d01ff7b1590b8c95 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 30 Jan 2015 17:15:44 +0800 Subject: [PATCH 157/279] Update badges --- README.rst | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 33c05c43..925531b4 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,19 @@ Flask-OAuthlib ============== -.. image:: https://badge.fury.io/py/Flask-OAuthlib.png - :target: http://badge.fury.io/py/Flask-OAuthlib -.. image:: https://travis-ci.org/lepture/flask-oauthlib.png?branch=master +.. image:: https://pypip.in/wheel/flask-oauthlib/badge.svg?style=flat + :target: https://pypi.python.org/pypi/flask-OAuthlib/ + :alt: Wheel Status +.. image:: https://pypip.in/version/flask-oauthlib/badge.svg?style=flat + :target: https://pypi.python.org/pypi/flask-oauthlib/ + :alt: Latest Version +.. image:: https://travis-ci.org/lepture/flask-oauthlib.svg?branch=master :target: https://travis-ci.org/lepture/flask-oauthlib -.. image:: https://coveralls.io/repos/lepture/flask-oauthlib/badge.png?branch=master + :alt: Travis CI Status +.. image:: https://coveralls.io/repos/lepture/flask-oauthlib/badge.svg?branch=master :target: https://coveralls.io/r/lepture/flask-oauthlib + :alt: Coverage Status + Flask-OAuthlib is an extension to Flask that allows you to interact with remote OAuth enabled applications. On the client site, it is a replacement From 62d4e0daca4742c4160b07be87cd7b098852e3cd Mon Sep 17 00:00:00 2001 From: alejandrodob Date: Fri, 30 Jan 2015 13:48:09 +0100 Subject: [PATCH 158/279] Use a local copy of instance 'request_token_params' attribute to avoid side effects --- flask_oauthlib/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 100c2740..4e506274 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -11,6 +11,7 @@ import logging import oauthlib.oauth1 import oauthlib.oauth2 +from copy import copy from functools import wraps from oauthlib.common import to_unicode, PY3, add_params_to_uri from flask import request, redirect, json, session, current_app @@ -339,7 +340,7 @@ def _get_property(self, key, default=False): def make_client(self, token=None): # request_token_url is for oauth1 if self.request_token_url: - params = self.request_token_params or {} + params = copy(self.request_token_params) or {} if token and isinstance(token, (tuple, list)): params["resource_owner_key"] = token[0] params["resource_owner_secret"] = token[1] From 443d2de9cc2345022c5fac5d1a4ea02788ce7d2e Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 3 Feb 2015 19:00:43 +0800 Subject: [PATCH 159/279] Register cache to app.extensions --- flask_oauthlib/contrib/cache.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask_oauthlib/contrib/cache.py b/flask_oauthlib/contrib/cache.py index 34259923..f8788aa7 100644 --- a/flask_oauthlib/contrib/cache.py +++ b/flask_oauthlib/contrib/cache.py @@ -20,6 +20,7 @@ def __init__(self, app, config_prefix='OAUTHLIB', **kwargs): raise RuntimeError( '`%s` is not a valid cache type!' % cache_type ) + app.extensions[config_prefix.lower() + '_cache'] = self.cache def __getattr__(self, key): try: From fe57f230da9a73351f66ebd7d8a39b2e6669dc40 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 3 Feb 2015 19:31:06 +0800 Subject: [PATCH 160/279] Version bump 0.9.0 --- CHANGES.rst | 241 +++++++++++++++++++++++++++++++++++++ MANIFEST.in | 1 + docs/changelog.rst | 223 +--------------------------------- flask_oauthlib/__init__.py | 2 +- 4 files changed, 244 insertions(+), 223 deletions(-) create mode 100644 CHANGES.rst diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 00000000..b67a16fc --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,241 @@ +Changelog +========= + +Here you can see the full list of changes between each Flask-OAuthlib release. + + +Version 0.9.0 +------------- + +Released on Feb 3, 2015 + +- New feature for contrib client, which will become the official client in + the future via `#136`_ and `#176`_. +- Add appropriate headers when making POST request for access toke via `#169`_. +- Use a local copy of instance 'request_token_params' attribute to avoid side + effects via `#177`_. +- Some minor fixes of contrib by Hsiaoming Yang. + +.. _`#177`: https://github.com/lepture/flask-oauthlib/pull/177 +.. _`#169`: https://github.com/lepture/flask-oauthlib/pull/169 +.. _`#136`: https://github.com/lepture/flask-oauthlib/pull/136 +.. _`#176`: https://github.com/lepture/flask-oauthlib/pull/176 + + +Version 0.8.0 +------------- + +Released on Dec 3, 2014 + +.. module:: flask_oauthlib.provider.oauth2 + +- New feature for generating refresh tokens +- Add new function :meth:`OAuth2Provider.verify_request` for non vanilla Flask projects +- Some small bugfixes + + +Version 0.7.0 +------------- + +Released on Aug 20, 2014 + +.. module:: flask_oauthlib.client + +- Deprecated :meth:`OAuthRemoteApp.authorized_handler` in favor of + :meth:`OAuthRemoteApp.authorized_response`. +- Add revocation endpoint via `#131`_. +- Handle unknown exceptions in providers. +- Add PATCH method for client via `#134`_. + +.. _`#131`: https://github.com/lepture/flask-oauthlib/pull/131 +.. _`#134`: https://github.com/lepture/flask-oauthlib/pull/134 + + +Version 0.6.0 +------------- + +Released on Jul 29, 2014 + +- Compatible with OAuthLib 0.6.2 and 0.6.3 +- Add invalid_response decorator to handle invalid request +- Add error_message for OAuthLib Request. + +Version 0.5.0 +------------- + +Released on May 13, 2014 + +- Add ``contrib.apps`` module, thanks for tonyseek via `#94`_. +- Status code changed to 401 for invalid access token via `#93`_. +- **Security bug** for access token via `#92`_. +- Fix for client part, request token params for OAuth1 via `#91`_. +- **API change** for ``oauth.require_oauth`` via `#89`_. +- Fix for OAuth2 provider, support client authentication for authorization-code grant type via `#86`_. +- Fix client_credentials logic in validate_grant_type via `#85`_. +- Fix for client part, pass access token method via `#83`_. +- Fix for OAuth2 provider related to confidential client via `#82`_. + +Upgrade From 0.4.x to 0.5.0 +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +API for OAuth providers ``oauth.require_oauth`` has changed. + +Before the change, you would write code like:: + + @app.route('/api/user') + @oauth.require_oauth('email') + def user(req): + return jsonify(req.user) + +After the change, you would write code like:: + + from flask import request + + @app.route('/api/user') + @oauth.require_oauth('email') + def user(): + return jsonify(request.oauth.user) + +.. _`#94`: https://github.com/lepture/flask-oauthlib/pull/94 +.. _`#93`: https://github.com/lepture/flask-oauthlib/issues/93 +.. _`#92`: https://github.com/lepture/flask-oauthlib/issues/92 +.. _`#91`: https://github.com/lepture/flask-oauthlib/issues/91 +.. _`#89`: https://github.com/lepture/flask-oauthlib/issues/89 +.. _`#86`: https://github.com/lepture/flask-oauthlib/pull/86 +.. _`#85`: https://github.com/lepture/flask-oauthlib/pull/85 +.. _`#83`: https://github.com/lepture/flask-oauthlib/pull/83 +.. _`#82`: https://github.com/lepture/flask-oauthlib/issues/82 + +Thanks Stian Prestholdt and Jiangge Zhang. + +Version 0.4.3 +------------- + +Released on Feb 18, 2014 + +- OAuthlib released 0.6.1, which caused a bug in oauth2 provider. +- Validation for scopes on oauth2 right via `#72`_. +- Handle empty response for application/json via `#69`_. + +.. _`#69`: https://github.com/lepture/flask-oauthlib/issues/69 +.. _`#72`: https://github.com/lepture/flask-oauthlib/issues/72 + +Version 0.4.2 +------------- + +Released on Jan 3, 2014 + +Happy New Year! + +- Add param ``state`` in authorize method via `#63`_. +- Bugfix for encoding error in Python 3 via `#65`_. + +.. _`#63`: https://github.com/lepture/flask-oauthlib/issues/63 +.. _`#65`: https://github.com/lepture/flask-oauthlib/issues/65 + +Version 0.4.1 +------------- + +Released on Nov 25, 2013 + +- Add access_token on request object via `#53`_. +- Bugfix for lazy loading configuration via `#55`_. + +.. _`#53`: https://github.com/lepture/flask-oauthlib/issues/53 +.. _`#55`: https://github.com/lepture/flask-oauthlib/issues/55 + + +Version 0.4.0 +------------- + +Released on Nov 12, 2013 + +- Redesign contrib library. +- A new way for lazy loading configuration via `#51`_. +- Some bugfixes. + +.. _`#51`: https://github.com/lepture/flask-oauthlib/issues/51 + + +Version 0.3.4 +------------- + +Released on Oct 31, 2013 + +- Bugfix for client missing a string placeholder via `#49`_. +- Bugfix for client property getter via `#48`_. + +.. _`#49`: https://github.com/lepture/flask-oauthlib/issues/49 +.. _`#48`: https://github.com/lepture/flask-oauthlib/issues/48 + +Version 0.3.3 +------------- + +Released on Oct 4, 2013 + +- Support for token generator in OAuth2 Provider via `#42`_. +- Improve client part, improve test cases. +- Fix scope via `#44`_. + +.. _`#42`: https://github.com/lepture/flask-oauthlib/issues/42 +.. _`#44`: https://github.com/lepture/flask-oauthlib/issues/44 + +Version 0.3.2 +------------- + +Released on Sep 13, 2013 + +- Upgrade oauthlib to 0.6 +- A quick bugfix for request token params via `#40`_. + +.. _`#40`: https://github.com/lepture/flask-oauthlib/issues/40 + +Version 0.3.1 +------------- + +Released on Aug 22, 2013 + +- Add contrib module via `#15`_. We are still working on it, + take your own risk. +- Add example of linkedin via `#35`_. +- Compatible with new proposals of oauthlib. +- Bugfix for client part. +- Backward compatible for lower version of Flask via `#37`_. + +.. _`#15`: https://github.com/lepture/flask-oauthlib/issues/15 +.. _`#35`: https://github.com/lepture/flask-oauthlib/issues/35 +.. _`#37`: https://github.com/lepture/flask-oauthlib/issues/37 + +Version 0.3.0 +------------- + +Released on July 10, 2013. + +- OAuth1 Provider available. Documentation at :doc:`oauth1`. :) +- Add ``before_request`` and ``after_request`` via `#22`_. +- Lazy load configuration for client via `#23`_. Documentation at :ref:`lazy-configuration`. +- Python 3 compatible now. + +.. _`#22`: https://github.com/lepture/flask-oauthlib/issues/22 +.. _`#23`: https://github.com/lepture/flask-oauthlib/issues/23 + +Version 0.2.0 +------------- + +Released on June 19, 2013. + +- OAuth2 Provider available. Documentation at :doc:`oauth2`. :) +- Make client part testable. +- Change extension name of client from ``oauth-client`` to ``oauthlib.client``. + +Version 0.1.1 +------------- + +Released on May 23, 2013. + +- Fix setup.py + +Version 0.1.0 +------------- + +First public preview release on May 18, 2013. diff --git a/MANIFEST.in b/MANIFEST.in index a5021c60..0a0fbdb4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include README.rst +include CHANGES.rst include LICENSE diff --git a/docs/changelog.rst b/docs/changelog.rst index 19122792..4e4d0e94 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,224 +1,3 @@ .. _changelog: -Changelog -========= - -Here you can see the full list of changes between each Flask-OAuthlib release. - -Version 0.8.0 -------------- - -Released on Dec 3, 2014 - -.. module:: flask_oauthlib.provider.oauth2 - -- New feature for generating refresh tokens -- Add new function :meth:`OAuth2Provider.verify_request` for non vanilla Flask projects -- Some small bugfixes - - -Version 0.7.0 -------------- - -Released on Aug 20, 2014 - -.. module:: flask_oauthlib.client - -- Deprecated :meth:`OAuthRemoteApp.authorized_handler` in favor of - :meth:`OAuthRemoteApp.authorized_response`. -- Add revocation endpoint via `#131`_. -- Handle unknown exceptions in providers. -- Add PATCH method for client via `#134`_. - -.. _`#131`: https://github.com/lepture/flask-oauthlib/pull/131 -.. _`#134`: https://github.com/lepture/flask-oauthlib/pull/134 - - -Version 0.6.0 -------------- - -Released on Jul 29, 2014 - -- Compatible with OAuthLib 0.6.2 and 0.6.3 -- Add invalid_response decorator to handle invalid request -- Add error_message for OAuthLib Request. - -Version 0.5.0 -------------- - -Released on May 13, 2014 - -- Add ``contrib.apps`` module, thanks for tonyseek via `#94`_. -- Status code changed to 401 for invalid access token via `#93`_. -- **Security bug** for access token via `#92`_. -- Fix for client part, request token params for OAuth1 via `#91`_. -- **API change** for ``oauth.require_oauth`` via `#89`_. -- Fix for OAuth2 provider, support client authentication for authorization-code grant type via `#86`_. -- Fix client_credentials logic in validate_grant_type via `#85`_. -- Fix for client part, pass access token method via `#83`_. -- Fix for OAuth2 provider related to confidential client via `#82`_. - -Upgrade From 0.4.x to 0.5.0 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -API for OAuth providers ``oauth.require_oauth`` has changed. - -Before the change, you would write code like:: - - @app.route('/api/user') - @oauth.require_oauth('email') - def user(req): - return jsonify(req.user) - -After the change, you would write code like:: - - from flask import request - - @app.route('/api/user') - @oauth.require_oauth('email') - def user(): - return jsonify(request.oauth.user) - -.. _`#94`: https://github.com/lepture/flask-oauthlib/pull/94 -.. _`#93`: https://github.com/lepture/flask-oauthlib/issues/93 -.. _`#92`: https://github.com/lepture/flask-oauthlib/issues/92 -.. _`#91`: https://github.com/lepture/flask-oauthlib/issues/91 -.. _`#89`: https://github.com/lepture/flask-oauthlib/issues/89 -.. _`#86`: https://github.com/lepture/flask-oauthlib/pull/86 -.. _`#85`: https://github.com/lepture/flask-oauthlib/pull/85 -.. _`#83`: https://github.com/lepture/flask-oauthlib/pull/83 -.. _`#82`: https://github.com/lepture/flask-oauthlib/issues/82 - -Thanks Stian Prestholdt and Jiangge Zhang. - -Version 0.4.3 -------------- - -Released on Feb 18, 2014 - -- OAuthlib released 0.6.1, which caused a bug in oauth2 provider. -- Validation for scopes on oauth2 right via `#72`_. -- Handle empty response for application/json via `#69`_. - -.. _`#69`: https://github.com/lepture/flask-oauthlib/issues/69 -.. _`#72`: https://github.com/lepture/flask-oauthlib/issues/72 - -Version 0.4.2 -------------- - -Released on Jan 3, 2014 - -Happy New Year! - -- Add param ``state`` in authorize method via `#63`_. -- Bugfix for encoding error in Python 3 via `#65`_. - -.. _`#63`: https://github.com/lepture/flask-oauthlib/issues/63 -.. _`#65`: https://github.com/lepture/flask-oauthlib/issues/65 - -Version 0.4.1 -------------- - -Released on Nov 25, 2013 - -- Add access_token on request object via `#53`_. -- Bugfix for lazy loading configuration via `#55`_. - -.. _`#53`: https://github.com/lepture/flask-oauthlib/issues/53 -.. _`#55`: https://github.com/lepture/flask-oauthlib/issues/55 - - -Version 0.4.0 -------------- - -Released on Nov 12, 2013 - -- Redesign contrib library. -- A new way for lazy loading configuration via `#51`_. -- Some bugfixes. - -.. _`#51`: https://github.com/lepture/flask-oauthlib/issues/51 - - -Version 0.3.4 -------------- - -Released on Oct 31, 2013 - -- Bugfix for client missing a string placeholder via `#49`_. -- Bugfix for client property getter via `#48`_. - -.. _`#49`: https://github.com/lepture/flask-oauthlib/issues/49 -.. _`#48`: https://github.com/lepture/flask-oauthlib/issues/48 - -Version 0.3.3 -------------- - -Released on Oct 4, 2013 - -- Support for token generator in OAuth2 Provider via `#42`_. -- Improve client part, improve test cases. -- Fix scope via `#44`_. - -.. _`#42`: https://github.com/lepture/flask-oauthlib/issues/42 -.. _`#44`: https://github.com/lepture/flask-oauthlib/issues/44 - -Version 0.3.2 -------------- - -Released on Sep 13, 2013 - -- Upgrade oauthlib to 0.6 -- A quick bugfix for request token params via `#40`_. - -.. _`#40`: https://github.com/lepture/flask-oauthlib/issues/40 - -Version 0.3.1 -------------- - -Released on Aug 22, 2013 - -- Add contrib module via `#15`_. We are still working on it, - take your own risk. -- Add example of linkedin via `#35`_. -- Compatible with new proposals of oauthlib. -- Bugfix for client part. -- Backward compatible for lower version of Flask via `#37`_. - -.. _`#15`: https://github.com/lepture/flask-oauthlib/issues/15 -.. _`#35`: https://github.com/lepture/flask-oauthlib/issues/35 -.. _`#37`: https://github.com/lepture/flask-oauthlib/issues/37 - -Version 0.3.0 -------------- - -Released on July 10, 2013. - -- OAuth1 Provider available. Documentation at :doc:`oauth1`. :) -- Add ``before_request`` and ``after_request`` via `#22`_. -- Lazy load configuration for client via `#23`_. Documentation at :ref:`lazy-configuration`. -- Python 3 compatible now. - -.. _`#22`: https://github.com/lepture/flask-oauthlib/issues/22 -.. _`#23`: https://github.com/lepture/flask-oauthlib/issues/23 - -Version 0.2.0 -------------- - -Released on June 19, 2013. - -- OAuth2 Provider available. Documentation at :doc:`oauth2`. :) -- Make client part testable. -- Change extension name of client from ``oauth-client`` to ``oauthlib.client``. - -Version 0.1.1 -------------- - -Released on May 23, 2013. - -- Fix setup.py - -Version 0.1.0 -------------- - -First public preview release on May 18, 2013. +.. _include: ../CHANGES.rst diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index 2b77ba12..154077b3 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -11,7 +11,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = "0.8.0" +__version__ = "0.9.0" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' From 9ab470f91abb507bd7518e2569514b4ba30e766a Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 3 Feb 2015 19:35:23 +0800 Subject: [PATCH 161/279] Fix changelog include --- docs/changelog.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4e4d0e94..d9e113ec 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,3 +1 @@ -.. _changelog: - -.. _include: ../CHANGES.rst +.. include:: ../CHANGES.rst From fabecc565dfda326a8a66b199b1d642a7bc674ad Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Wed, 4 Feb 2015 17:06:41 +0800 Subject: [PATCH 162/279] there is a missing argument. remove useless data. --- flask_oauthlib/contrib/client/application.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py index b7530a23..aa734ff0 100644 --- a/flask_oauthlib/contrib/client/application.py +++ b/flask_oauthlib/contrib/client/application.py @@ -8,7 +8,6 @@ import os import contextlib import warnings -import functools try: from urllib.parse import urljoin except ImportError: @@ -82,7 +81,7 @@ def client(self): token = self.obtain_token() if token is None: raise AccessTokenNotFound - return self._make_client_with_token() + return self._make_client_with_token(token) def _make_client_with_token(self, token): """Uses cached client or create new one with specific token.""" @@ -117,15 +116,6 @@ def authorized_response(self): """ raise NotImplementedError - forward_methods = frozenset([ - 'head', - 'get', - 'post', - 'put', - 'delete', - 'patch', - ]) - def request(self, method, url, token=None, *args, **kwargs): if token is None: client = self.client From 069b2f47adf9ea2df7f4e6894efe6573675dcb6e Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 15 Feb 2015 06:54:53 +0800 Subject: [PATCH 163/279] fix up the broken auto-refresh feature. --- flask_oauthlib/contrib/client/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py index aa734ff0..f792714e 100644 --- a/flask_oauthlib/contrib/client/application.py +++ b/flask_oauthlib/contrib/client/application.py @@ -246,7 +246,7 @@ def make_client(self, token): :returns: a :class:`requests_oauthlib.oauth2_session.OAuth2Session` object. """ - return self.session_class(self.client_id, token=token) + return self.make_oauth_session(token=token) def tokensaver(self, fn): """A decorator to register a callback function for saving refreshed From 7e0dcfa75bf44422bd191056af0e2b569e57a316 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sat, 28 Feb 2015 13:55:39 +0800 Subject: [PATCH 164/279] Change default access token method to POST https://github.com/lepture/flask-oauthlib/issues/184 --- flask_oauthlib/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 4e506274..7899647c 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -310,7 +310,7 @@ def access_token_params(self): @cached_property def access_token_method(self): - return self._get_property('access_token_method', 'GET') + return self._get_property('access_token_method', 'POST') @cached_property def content_type(self): From 829fdea095d2dac9cc03eb41b2e9946225c995fb Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sun, 1 Mar 2015 01:04:31 +0800 Subject: [PATCH 165/279] Verify client_secret on authorization_code response type --- flask_oauthlib/provider/oauth2.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index ecd3d088..6f869909 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -643,6 +643,11 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): log.debug('Authenticate failed, client not found.') return False + if request.grant_type == 'authorization_code' and \ + client.client_secret != request.client_secret: + log.debug('Authenticate client failed, secret not match.') + return False + # attach client on request for convenience request.client = client return True From 8a31fe28ca62269a8b069ce5110cf52af10733d0 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 10 Mar 2015 17:07:37 +0800 Subject: [PATCH 166/279] Version bump 0.9.1 --- CHANGES.rst | 8 ++++++++ flask_oauthlib/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index b67a16fc..d8a1adda 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,14 @@ Changelog Here you can see the full list of changes between each Flask-OAuthlib release. +Version 0.9.1 +------------- + +Released on Mar 9, 2015 + +- Improve on security. +- Fix on contrib client. + Version 0.9.0 ------------- diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index 154077b3..73a182d3 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -11,7 +11,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = "0.9.0" +__version__ = "0.9.1" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' From 32e799bf795f9e49f7bb85c0262b5af29a0fd9a4 Mon Sep 17 00:00:00 2001 From: Clinton Dreisbach Date: Tue, 10 Mar 2015 12:01:08 -0400 Subject: [PATCH 167/279] Change test for no content when parsing response When parsing the response for a DELETE request to GitHub, I found I had an empty byte-string instead of an empty string for my response data. This caused OAuthlib to try and parse it with JSON, which will not work. Changing the test to allow for any Falsey value will allow empty strings or byte-strings to return an empty dict. --- flask_oauthlib/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 7899647c..80e028e3 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -117,7 +117,7 @@ def parse_response(resp, content, strict=False, content_type=None): ct, options = parse_options_header(content_type) if ct in ('application/json', 'text/javascript'): - if content == '': + if not content: return {} return json.loads(content) From 78979c6a46472b933696f8a110b05726e7cd0f4b Mon Sep 17 00:00:00 2001 From: Kanstantsin Kamkou Date: Tue, 17 Mar 2015 21:52:05 +0100 Subject: [PATCH 168/279] Update github.py There is no error_reason --- example/github.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/github.py b/example/github.py index 119770b7..63e4af24 100644 --- a/example/github.py +++ b/example/github.py @@ -44,7 +44,7 @@ def authorized(): resp = github.authorized_response() if resp is None: return 'Access denied: reason=%s error=%s' % ( - request.args['error_reason'], + request.args['error'], request.args['error_description'] ) session['github_token'] = (resp['access_token'], '') From 64d82c7d1fc9118c904940b0c5aa49cb5180a277 Mon Sep 17 00:00:00 2001 From: Gordon Fierce Date: Thu, 26 Mar 2015 13:11:05 -0400 Subject: [PATCH 169/279] Fix minor typo with tense. --- docs/oauth2.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/oauth2.rst b/docs/oauth2.rst index c84cda76..cd95a125 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -157,7 +157,7 @@ Also in SQLAlchemy model (would be better if it is in a cache):: Bearer Token ------------ -A bearer token is the final token that could be use by the client. There +A bearer token is the final token that could be used by the client. There are other token types, but bearer token is widely used. Flask-OAuthlib only comes with bearer token. @@ -429,9 +429,9 @@ The authorization flow is finished, everything should be working now. Revoke handler `````````````` -In some cases a user may wish to revoke access given to an application and the -revoke handler makes it possible for an application to programmaticaly revoke -the access given to it. Also here you don't need to do much, allowing POST only +In some cases a user may wish to revoke access given to an application and the +revoke handler makes it possible for an application to programmaticaly revoke +the access given to it. Also here you don't need to do much, allowing POST only is recommended:: @app.route('/oauth/revoke', methods=['POST']) From e08b7d7b3a495694116a1b8b3e20cc9e6a32082b Mon Sep 17 00:00:00 2001 From: Li Yazhou Date: Wed, 1 Apr 2015 14:35:02 +0800 Subject: [PATCH 170/279] authenticate confidential client on grant_type == 'authentication_code' ref: http://tools.ietf.org/html/rfc6749#section-3.2.1 > For example, the client makes the following HTTP request using TLS > (with extra line breaks for display purposes only): > > POST /token HTTP/1.1 > Host: server.example.com > Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW > Content-Type: application/x-www-form-urlencoded > > grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA > &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb > > The authorization server MUST: > > o require client authentication for confidential clients or for any > client that was issued client credentials (or with other > authentication requirements), --- flask_oauthlib/provider/oauth2.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 6f869909..3fc783d3 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -583,14 +583,14 @@ def client_authentication_required(self, request, *args, **kwargs): .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 """ + client = self._clientgetter(request.client_id) if request.grant_type == 'password': - client = self._clientgetter(request.client_id) - return (not client) or client.client_type == 'confidential' or\ - request.client_secret - - auth_required = ('authorization_code', 'refresh_token') - return 'Authorization' in request.headers and\ - request.grant_type in auth_required + return (not client) or client.client_type == 'confidential' \ + or client.client_secret + elif request.grant_type == 'authorization_code': + return (not client) or client.client_type == 'confidential' + return 'Authorization' in request.headers \ + and request.grant_type == 'refresh_token' def authenticate_client(self, request, *args, **kwargs): """Authenticate itself in other means. From 12b1990aa4eb95425885f5846cf4154408c80c69 Mon Sep 17 00:00:00 2001 From: Li Yazhou Date: Wed, 1 Apr 2015 16:01:21 +0800 Subject: [PATCH 171/279] mvoe get client inside --- flask_oauthlib/provider/oauth2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 3fc783d3..f1f6188f 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -583,11 +583,12 @@ def client_authentication_required(self, request, *args, **kwargs): .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 """ - client = self._clientgetter(request.client_id) if request.grant_type == 'password': + client = self._clientgetter(request.client_id) return (not client) or client.client_type == 'confidential' \ or client.client_secret elif request.grant_type == 'authorization_code': + client = self._clientgetter(request.client_id) return (not client) or client.client_type == 'confidential' return 'Authorization' in request.headers \ and request.grant_type == 'refresh_token' From dbc00f3cc76c322ad09d1d7c23883af0f681775e Mon Sep 17 00:00:00 2001 From: Li Yazhou Date: Wed, 1 Apr 2015 22:07:26 +0800 Subject: [PATCH 172/279] do not check secret for non credential clients --- flask_oauthlib/provider/oauth2.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index f1f6188f..86dbf532 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -644,11 +644,6 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): log.debug('Authenticate failed, client not found.') return False - if request.grant_type == 'authorization_code' and \ - client.client_secret != request.client_secret: - log.debug('Authenticate client failed, secret not match.') - return False - # attach client on request for convenience request.client = client return True From e82c0b4509304e458f431ca1ec355d6c1972cfb1 Mon Sep 17 00:00:00 2001 From: Li Yazhou Date: Wed, 1 Apr 2015 22:29:37 +0800 Subject: [PATCH 173/279] do not check client_type inside authenticate_client() as `client_authentication_required()` describes, a non credential client might also enters `authenticate_client()`. --- flask_oauthlib/provider/oauth2.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 86dbf532..9e189b93 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -626,9 +626,6 @@ def authenticate_client(self, request, *args, **kwargs): log.debug('Authenticate client failed, secret not match.') return False - if client.client_type != 'confidential': - log.debug('Authenticate client failed, not confidential.') - return False log.debug('Authenticate client success.') return True From 4914e46cb8e3b25fdaba4fac1383e2993bde128d Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Apr 2015 13:15:32 +0800 Subject: [PATCH 174/279] Refactor testing providers --- tests/test_oauth2/__init__.py | 0 tests/test_oauth2/base.py | 312 +++++++++++++++++++++++++++++++++ tests/test_oauth2/test_code.py | 140 +++++++++++++++ 3 files changed, 452 insertions(+) create mode 100644 tests/test_oauth2/__init__.py create mode 100644 tests/test_oauth2/base.py create mode 100644 tests/test_oauth2/test_code.py diff --git a/tests/test_oauth2/__init__.py b/tests/test_oauth2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_oauth2/base.py b/tests/test_oauth2/base.py new file mode 100644 index 00000000..60c48d61 --- /dev/null +++ b/tests/test_oauth2/base.py @@ -0,0 +1,312 @@ +# coding: utf-8 + +import os +import unittest +from datetime import datetime, timedelta +from flask import Flask +from flask import g, render_template, request, jsonify, make_response +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.orm import relationship +from flask_oauthlib.provider import OAuth2Provider +from flask_oauthlib.contrib.oauth2 import bind_sqlalchemy +from flask_oauthlib.contrib.oauth2 import bind_cache_grant + +os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = 'true' + +db = SQLAlchemy() + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(40), unique=True, index=True, + nullable=False) + + def check_password(self, password): + return True + + +class Client(db.Model): + # id = db.Column(db.Integer, primary_key=True) + # human readable name + name = db.Column(db.String(40)) + client_id = db.Column(db.String(40), primary_key=True) + client_secret = db.Column(db.String(55), unique=True, index=True, + nullable=False) + is_confidential = db.Column(db.Boolean, default=False) + _redirect_uris = db.Column(db.Text) + default_scope = db.Column(db.Text, default='email address') + + @property + def client_type(self): + if self.is_confidential: + return 'confidential' + return 'public' + + @property + def user(self): + return User.query.get(1) + + @property + def redirect_uris(self): + if self._redirect_uris: + return self._redirect_uris.split() + return [] + + @property + def default_redirect_uri(self): + return self.redirect_uris[0] + + @property + def default_scopes(self): + if self.default_scope: + return self.default_scope.split() + return [] + + @property + def allowed_grant_types(self): + return ['authorization_code', 'password', 'client_credentials', + 'refresh_token'] + + +class Grant(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column( + db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') + ) + user = relationship('User') + + client_id = db.Column( + db.String(40), db.ForeignKey('client.client_id', ondelete='CASCADE'), + nullable=False, + ) + client = relationship('Client') + code = db.Column(db.String(255), index=True, nullable=False) + + redirect_uri = db.Column(db.String(255)) + scope = db.Column(db.Text) + expires = db.Column(db.DateTime) + + def delete(self): + db.session.delete(self) + db.session.commit() + return self + + @property + def scopes(self): + if self.scope: + return self.scope.split() + return None + + +class Token(db.Model): + id = db.Column(db.Integer, primary_key=True) + client_id = db.Column( + db.String(40), db.ForeignKey('client.client_id', ondelete='CASCADE'), + nullable=False, + ) + user_id = db.Column( + db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') + ) + user = relationship('User') + client = relationship('Client') + token_type = db.Column(db.String(40)) + access_token = db.Column(db.String(255)) + refresh_token = db.Column(db.String(255)) + expires = db.Column(db.DateTime) + scope = db.Column(db.Text) + + def __init__(self, **kwargs): + expires_in = kwargs.pop('expires_in') + self.expires = datetime.utcnow() + timedelta(seconds=expires_in) + for k, v in kwargs.items(): + setattr(self, k, v) + + @property + def scopes(self): + if self.scope: + return self.scope.split() + return [] + + def delete(self): + db.session.delete(self) + db.session.commit() + return self + + +def current_user(): + return g.user + + +def cache_provider(app): + oauth = OAuth2Provider(app) + + bind_sqlalchemy(oauth, db.session, user=User, + token=Token, client=Client) + + app.config.update({'OAUTH2_CACHE_TYPE': 'simple'}) + bind_cache_grant(app, oauth, current_user) + return oauth + + +def sqlalchemy_provider(app): + oauth = OAuth2Provider(app) + + bind_sqlalchemy(oauth, db.session, user=User, token=Token, + client=Client, grant=Grant, current_user=current_user) + + return oauth + + +def default_provider(app): + oauth = OAuth2Provider(app) + + @oauth.clientgetter + def get_client(client_id): + return Client.query.filter_by(client_id=client_id).first() + + @oauth.grantgetter + def get_grant(client_id, code): + return Grant.query.filter_by(client_id=client_id, code=code).first() + + @oauth.tokengetter + def get_token(access_token=None, refresh_token=None): + if access_token: + return Token.query.filter_by(access_token=access_token).first() + if refresh_token: + return Token.query.filter_by(refresh_token=refresh_token).first() + return None + + @oauth.grantsetter + def set_grant(client_id, code, request, *args, **kwargs): + expires = datetime.utcnow() + timedelta(seconds=100) + grant = Grant( + client_id=client_id, + code=code['code'], + redirect_uri=request.redirect_uri, + scope=' '.join(request.scopes), + user_id=g.user.id, + expires=expires, + ) + db.session.add(grant) + db.session.commit() + + @oauth.tokensetter + def set_token(token, request, *args, **kwargs): + # In real project, a token is unique bound to user and client. + # Which means, you don't need to create a token every time. + tok = Token(**token) + tok.user_id = request.user.id + tok.client_id = request.client.client_id + db.session.add(tok) + db.session.commit() + + @oauth.usergetter + def get_user(username, password, *args, **kwargs): + # This is optional, if you don't need password credential + # there is no need to implement this method + return User.query.filter_by(username=username).first() + + return oauth + + +def create_server(app, oauth=None): + if not oauth: + oauth = default_provider(app) + + @app.before_request + def load_current_user(): + user = User.query.get(1) + g.user = user + + @app.route('/home') + def home(): + return render_template('home.html') + + @app.route('/oauth/authorize', methods=['GET', 'POST']) + @oauth.authorize_handler + def authorize(*args, **kwargs): + # NOTICE: for real project, you need to require login + if request.method == 'GET': + # render a page for user to confirm the authorization + return 'confirm page' + + if request.method == 'HEAD': + # if HEAD is supported properly, request parameters like + # client_id should be validated the same way as for 'GET' + response = make_response('', 200) + response.headers['X-Client-ID'] = kwargs.get('client_id') + return response + + confirm = request.form.get('confirm', 'no') + return confirm == 'yes' + + @app.route('/oauth/token', methods=['POST', 'GET']) + @oauth.token_handler + def access_token(): + return {} + + @app.route('/oauth/revoke', methods=['POST']) + @oauth.revoke_handler + def revoke_token(): + return {} + + @app.route('/api/email') + @oauth.require_oauth('email') + def email_api(): + oauth = request.oauth + return jsonify(email='me@oauth.net', username=oauth.user.username) + + @app.route('/api/client') + @oauth.require_oauth() + def client_api(): + oauth = request.oauth + return jsonify(client=oauth.client.name) + + @app.route('/api/address/') + @oauth.require_oauth('address') + def address_api(city): + oauth = request.oauth + return jsonify(address=city, username=oauth.user.username) + + @app.route('/api/method', methods=['GET', 'POST', 'PUT', 'DELETE']) + @oauth.require_oauth() + def method_api(): + return jsonify(method=request.method) + + @oauth.invalid_response + def require_oauth_invalid(req): + return jsonify(message=req.error_message), 401 + + return app + + +class TestCase(unittest.TestCase): + def setUp(self): + app = self.create_app() + + app.testing = True + self._ctx = app.app_context() + self._ctx.push() + + db.init_app(app) + db.create_all() + + self.app = app + self.client = app.test_client() + self.prepare_data() + + def tearDown(self): + db.drop_all() + self._ctx.pop() + + def prepare_data(self): + return True + + def create_app(self): + app = Flask(__name__) + app.debug = True + app.secret_key = 'testing' + app.config.update({ + 'SQLALCHEMY_DATABASE_URI': 'sqlite://' + }) + return app diff --git a/tests/test_oauth2/test_code.py b/tests/test_oauth2/test_code.py new file mode 100644 index 00000000..9a93e5c4 --- /dev/null +++ b/tests/test_oauth2/test_code.py @@ -0,0 +1,140 @@ +# coding: utf-8 + +from datetime import datetime, timedelta +from .base import TestCase +from .base import create_server, sqlalchemy_provider, cache_provider +from .base import db, Client, User, Grant + + +class TestCodeDefaultProvider(TestCase): + def create_server(self): + create_server(self.app) + + def prepare_data(self): + self.create_server() + + oauth_client = Client( + name='ios', client_id='code-client', client_secret='code-secret', + _redirect_uris='http://localhost/authorized', + ) + + db.session.add(User(username='foo')) + db.session.add(oauth_client) + + self.oauth_client = oauth_client + self.authorize_url = ( + '/oauth/authorize?response_type=code&client_id=%s' + ) % oauth_client.client_id + + def test_get_authorize(self): + rv = self.client.get('/oauth/authorize') + assert 'invalid_client_id' in rv.location + + rv = self.client.get('/oauth/authorize?client_id=no') + assert 'invalid_client_id' in rv.location + + url = '/oauth/authorize?client_id=%s' % self.oauth_client.client_id + rv = self.client.get(url) + assert 'error' in rv.location + + rv = self.client.get(self.authorize_url) + assert b'confirm' in rv.data + + def test_post_authorize(self): + url = self.authorize_url + '&scope=foo' + rv = self.client.post(url, data={'confirm': 'yes'}) + assert 'invalid_scope' in rv.location + + url = self.authorize_url + '&scope=email' + rv = self.client.post(url, data={'confirm': 'yes'}) + assert 'code' in rv.location + + def test_invalid_token(self): + rv = self.client.get('/oauth/token') + assert b'unsupported_grant_type' in rv.data + + rv = self.client.get('/oauth/token?grant_type=authorization_code') + assert b'error' in rv.data + assert b'code' in rv.data + + url = ( + '/oauth/token?grant_type=authorization_code' + '&code=nothing&client_id=%s' + ) % self.oauth_client.client_id + rv = self.client.get(url) + assert b'invalid_client' in rv.data + + url += '&client_secret=' + self.oauth_client.client_secret + rv = self.client.get(url) + assert b'invalid_client' not in rv.data + assert rv.status_code == 401 + + def test_get_token(self): + expires = datetime.utcnow() + timedelta(seconds=100) + grant = Grant( + user_id=1, + client_id=self.oauth_client.client_id, + scope='email', + redirect_uri='http://localhost/authorized', + code='test-get-token', + expires=expires, + ) + db.session.add(grant) + db.session.commit() + + url = ( + '/oauth/token?grant_type=authorization_code' + '&code=test-get-token&client_id=%s' + ) % self.oauth_client.client_id + rv = self.client.get(url) + assert b'invalid_client' in rv.data + + url += '&client_secret=' + self.oauth_client.client_secret + rv = self.client.get(url) + assert b'access_token' in rv.data + + +class TestCodeSQLAlchemyProvider(TestCodeDefaultProvider): + def create_server(self): + create_server(self.app, sqlalchemy_provider(self.app)) + + +class TestCodeCacheProvider(TestCodeDefaultProvider): + def create_server(self): + create_server(self.app, cache_provider(self.app)) + + def test_get_token(self): + url = self.authorize_url + '&scope=email' + rv = self.client.post(url, data={'confirm': 'yes'}) + assert 'code' in rv.location + code = rv.location.split('code=')[1] + + url = ( + '/oauth/token?grant_type=authorization_code' + '&code=%s&client_id=%s' + ) % (code, self.oauth_client.client_id) + rv = self.client.get(url) + assert b'invalid_client' in rv.data + + url += '&client_secret=' + self.oauth_client.client_secret + rv = self.client.get(url) + assert b'access_token' in rv.data + + +class TestCodeConfidential(TestCodeDefaultProvider): + def prepare_data(self): + self.create_server() + + oauth_client = Client( + name='ios', client_id='code-client', client_secret='code-secret', + is_confidential=True, + _redirect_uris='http://localhost/authorized', + ) + + db.session.add(User(username='foo')) + db.session.add(oauth_client) + + self.oauth_client = oauth_client + self.authorize_url = ( + '/oauth/authorize?response_type=code&client_id=%s' + ) % oauth_client.client_id From d66849276d37552cdd02473924de875a794edca6 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Apr 2015 14:19:23 +0800 Subject: [PATCH 175/279] Fix for authenticate client --- flask_oauthlib/provider/oauth2.py | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 9e189b93..f14361bf 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -232,7 +232,6 @@ def clientgetter(self, f): - client_id: A random string - client_secret: A random string - - client_type: A string represents if it is `confidential` - redirect_uris: A list of redirect uris - default_redirect_uri: One of the redirect uris - default_scopes: Default scopes of the client @@ -582,24 +581,10 @@ def client_authentication_required(self, request, *args, **kwargs): .. _`Section 4.1.3`: http://tools.ietf.org/html/rfc6749#section-4.1.3 .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 """ - - if request.grant_type == 'password': - client = self._clientgetter(request.client_id) - return (not client) or client.client_type == 'confidential' \ - or client.client_secret - elif request.grant_type == 'authorization_code': - client = self._clientgetter(request.client_id) - return (not client) or client.client_type == 'confidential' - return 'Authorization' in request.headers \ - and request.grant_type == 'refresh_token' + grant_types = ('password', 'authorization_code', 'refresh_token') + return request.grant_type in grant_types def authenticate_client(self, request, *args, **kwargs): - """Authenticate itself in other means. - - Other means means is described in `Section 3.2.1`_. - - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 - """ auth = request.headers.get('Authorization', None) log.debug('Authenticate client %r', auth) if auth: @@ -617,15 +602,13 @@ def authenticate_client(self, request, *args, **kwargs): client = self._clientgetter(client_id) if not client: - log.debug('Authenticate client failed, client not found.') return False - request.client = client - if client.client_secret != client_secret: log.debug('Authenticate client failed, secret not match.') return False + request.client = client log.debug('Authenticate client success.') return True @@ -635,8 +618,9 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): :param client_id: Client ID of the non-confidential client :param request: The Request object passed by oauthlib """ - log.debug('Authenticate client %r.', client_id) - client = request.client or self._clientgetter(client_id) + log.debug('Authenticate client id %r.', client_id) + + client = self._clientgetter(client_id) if not client: log.debug('Authenticate failed, client not found.') return False From 764b58099e1f6de00ce27183d6d59da5f1c92127 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Apr 2015 14:43:09 +0800 Subject: [PATCH 176/279] Update and add test password --- tests/test_oauth2/base.py | 14 ++---- tests/test_oauth2/test_code.py | 25 ++------- tests/test_oauth2/test_password.py | 81 ++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 tests/test_oauth2/test_password.py diff --git a/tests/test_oauth2/base.py b/tests/test_oauth2/base.py index 60c48d61..8bc6e3b7 100644 --- a/tests/test_oauth2/base.py +++ b/tests/test_oauth2/base.py @@ -22,7 +22,7 @@ class User(db.Model): nullable=False) def check_password(self, password): - return True + return password != 'wrong' class Client(db.Model): @@ -32,16 +32,9 @@ class Client(db.Model): client_id = db.Column(db.String(40), primary_key=True) client_secret = db.Column(db.String(55), unique=True, index=True, nullable=False) - is_confidential = db.Column(db.Boolean, default=False) _redirect_uris = db.Column(db.Text) default_scope = db.Column(db.Text, default='email address') - @property - def client_type(self): - if self.is_confidential: - return 'confidential' - return 'public' - @property def user(self): return User.query.get(1) @@ -204,7 +197,10 @@ def set_token(token, request, *args, **kwargs): def get_user(username, password, *args, **kwargs): # This is optional, if you don't need password credential # there is no need to implement this method - return User.query.filter_by(username=username).first() + user = User.query.filter_by(username=username).first() + if user and user.check_password(password): + return user + return None return oauth diff --git a/tests/test_oauth2/test_code.py b/tests/test_oauth2/test_code.py index 9a93e5c4..617cf189 100644 --- a/tests/test_oauth2/test_code.py +++ b/tests/test_oauth2/test_code.py @@ -6,7 +6,7 @@ from .base import db, Client, User, Grant -class TestCodeDefaultProvider(TestCase): +class TestDefaultProvider(TestCase): def create_server(self): create_server(self.app) @@ -94,12 +94,12 @@ def test_get_token(self): assert b'access_token' in rv.data -class TestCodeSQLAlchemyProvider(TestCodeDefaultProvider): +class TestSQLAlchemyProvider(TestDefaultProvider): def create_server(self): create_server(self.app, sqlalchemy_provider(self.app)) -class TestCodeCacheProvider(TestCodeDefaultProvider): +class TestCacheProvider(TestDefaultProvider): def create_server(self): create_server(self.app, cache_provider(self.app)) @@ -119,22 +119,3 @@ def test_get_token(self): url += '&client_secret=' + self.oauth_client.client_secret rv = self.client.get(url) assert b'access_token' in rv.data - - -class TestCodeConfidential(TestCodeDefaultProvider): - def prepare_data(self): - self.create_server() - - oauth_client = Client( - name='ios', client_id='code-client', client_secret='code-secret', - is_confidential=True, - _redirect_uris='http://localhost/authorized', - ) - - db.session.add(User(username='foo')) - db.session.add(oauth_client) - - self.oauth_client = oauth_client - self.authorize_url = ( - '/oauth/authorize?response_type=code&client_id=%s' - ) % oauth_client.client_id diff --git a/tests/test_oauth2/test_password.py b/tests/test_oauth2/test_password.py new file mode 100644 index 00000000..6a857c6d --- /dev/null +++ b/tests/test_oauth2/test_password.py @@ -0,0 +1,81 @@ +# coding: utf-8 + +from .base import TestCase +from .base import create_server, sqlalchemy_provider, cache_provider +from .base import db, Client, User + + +class TestDefaultProvider(TestCase): + def create_server(self): + create_server(self.app) + + def prepare_data(self): + self.create_server() + + oauth_client = Client( + name='ios', client_id='pass-client', client_secret='pass-secret', + _redirect_uris='http://localhost/authorized', + ) + + db.session.add(User(username='foo')) + db.session.add(oauth_client) + + self.oauth_client = oauth_client + + def test_invalid_username(self): + rv = self.client.post('/oauth/token', data={ + 'grant_type': 'password', + 'username': 'notfound', + 'password': 'right', + 'client_id': self.oauth_client.client_id, + 'client_secret': self.oauth_client.client_secret, + }) + assert b'error' in rv.data + + def test_invalid_password(self): + rv = self.client.post('/oauth/token', data={ + 'grant_type': 'password', + 'username': 'foo', + 'password': 'wrong', + 'client_id': self.oauth_client.client_id, + 'client_secret': self.oauth_client.client_secret, + }) + assert b'error' in rv.data + + def test_missing_client_secret(self): + rv = self.client.post('/oauth/token', data={ + 'grant_type': 'password', + 'username': 'foo', + 'password': 'wrong', + 'client_id': self.oauth_client.client_id, + }) + assert b'error' in rv.data + + def test_get_token(self): + rv = self.client.post('/oauth/token', data={ + 'grant_type': 'password', + 'username': 'foo', + 'password': 'right', + 'client_id': self.oauth_client.client_id, + 'client_secret': self.oauth_client.client_secret, + }) + assert b'access_token' in rv.data + + # in Authorization + auth = 'cGFzcy1jbGllbnQ6cGFzcy1zZWNyZXQ=' + rv = self.client.post('/oauth/token', data={ + 'grant_type': 'password', + 'username': 'foo', + 'password': 'right', + }, headers={'Authorization': 'Basic %s' % auth}) + assert b'access_token' in rv.data + + +class TestSQLAlchemyProvider(TestDefaultProvider): + def create_server(self): + create_server(self.app, sqlalchemy_provider(self.app)) + + +class TestCacheProvider(TestDefaultProvider): + def create_server(self): + create_server(self.app, cache_provider(self.app)) From fb764a3540ad547598fbcdacadfb51b43348bf6d Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Apr 2015 14:55:44 +0800 Subject: [PATCH 177/279] Testing for allowed grant types --- tests/test_oauth2/base.py | 10 ++++++++-- tests/test_oauth2/test_password.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/test_oauth2/base.py b/tests/test_oauth2/base.py index 8bc6e3b7..9915d7b3 100644 --- a/tests/test_oauth2/base.py +++ b/tests/test_oauth2/base.py @@ -34,6 +34,7 @@ class Client(db.Model): nullable=False) _redirect_uris = db.Column(db.Text) default_scope = db.Column(db.Text, default='email address') + disallow_grant_type = db.Column(db.String(20)) @property def user(self): @@ -57,8 +58,13 @@ def default_scopes(self): @property def allowed_grant_types(self): - return ['authorization_code', 'password', 'client_credentials', - 'refresh_token'] + types = [ + 'authorization_code', 'password', + 'client_credentials', 'refresh_token', + ] + if self.disallow_grant_type: + types.remove(self.disallow_grant_type) + return types class Grant(db.Model): diff --git a/tests/test_oauth2/test_password.py b/tests/test_oauth2/test_password.py index 6a857c6d..20c11b57 100644 --- a/tests/test_oauth2/test_password.py +++ b/tests/test_oauth2/test_password.py @@ -70,6 +70,20 @@ def test_get_token(self): }, headers={'Authorization': 'Basic %s' % auth}) assert b'access_token' in rv.data + def test_disallow_grant_type(self): + self.oauth_client.disallow_grant_type = 'password' + db.session.add(self.oauth_client) + db.session.commit() + + rv = self.client.post('/oauth/token', data={ + 'grant_type': 'password', + 'username': 'foo', + 'password': 'right', + 'client_id': self.oauth_client.client_id, + 'client_secret': self.oauth_client.client_secret, + }) + assert b'error' in rv.data + class TestSQLAlchemyProvider(TestDefaultProvider): def create_server(self): From 4442b058a293197e36c3d132c1c1ffcb6fbd9ea2 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Apr 2015 15:13:53 +0800 Subject: [PATCH 178/279] Testing for implicit response type --- tests/test_oauth2/base.py | 7 +++-- tests/test_oauth2/test_implicit.py | 43 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/test_oauth2/test_implicit.py diff --git a/tests/test_oauth2/base.py b/tests/test_oauth2/base.py index 9915d7b3..60ba0218 100644 --- a/tests/test_oauth2/base.py +++ b/tests/test_oauth2/base.py @@ -140,7 +140,7 @@ def cache_provider(app): oauth = OAuth2Provider(app) bind_sqlalchemy(oauth, db.session, user=User, - token=Token, client=Client) + token=Token, client=Client, current_user=current_user) app.config.update({'OAUTH2_CACHE_TYPE': 'simple'}) bind_cache_grant(app, oauth, current_user) @@ -194,7 +194,10 @@ def set_token(token, request, *args, **kwargs): # In real project, a token is unique bound to user and client. # Which means, you don't need to create a token every time. tok = Token(**token) - tok.user_id = request.user.id + if request.response_type == 'token': + tok.user_id = g.user.id + else: + tok.user_id = request.user.id tok.client_id = request.client.client_id db.session.add(tok) db.session.commit() diff --git a/tests/test_oauth2/test_implicit.py b/tests/test_oauth2/test_implicit.py new file mode 100644 index 00000000..919ef907 --- /dev/null +++ b/tests/test_oauth2/test_implicit.py @@ -0,0 +1,43 @@ +# coding: utf-8 + +from .base import TestCase +from .base import create_server, sqlalchemy_provider, cache_provider +from .base import db, Client, User + + +class TestDefaultProvider(TestCase): + def create_server(self): + create_server(self.app) + + def prepare_data(self): + self.create_server() + + oauth_client = Client( + name='ios', client_id='imp-client', client_secret='imp-secret', + _redirect_uris='http://localhost/authorized', + ) + + db.session.add(User(username='foo')) + db.session.add(oauth_client) + + self.oauth_client = oauth_client + + def test_implicit(self): + rv = self.client.post('/oauth/authorize', data={ + 'response_type': 'token', + 'confirm': 'yes', + 'scope': 'email', + 'client_id': self.oauth_client.client_id, + 'client_secret': self.oauth_client.client_secret, + }) + assert 'access_token' in rv.location + + +class TestSQLAlchemyProvider(TestDefaultProvider): + def create_server(self): + create_server(self.app, sqlalchemy_provider(self.app)) + + +class TestCacheProvider(TestDefaultProvider): + def create_server(self): + create_server(self.app, cache_provider(self.app)) From 3f948694bdb8371a97c049638e3b930ec7764678 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Apr 2015 15:16:51 +0800 Subject: [PATCH 179/279] Add test client_credential --- tests/test_oauth2/test_client_credential.py | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/test_oauth2/test_client_credential.py diff --git a/tests/test_oauth2/test_client_credential.py b/tests/test_oauth2/test_client_credential.py new file mode 100644 index 00000000..bf11e2e7 --- /dev/null +++ b/tests/test_oauth2/test_client_credential.py @@ -0,0 +1,41 @@ +# coding: utf-8 + +from .base import TestCase +from .base import create_server, sqlalchemy_provider, cache_provider +from .base import db, Client, User + + +class TestDefaultProvider(TestCase): + def create_server(self): + create_server(self.app) + + def prepare_data(self): + self.create_server() + + oauth_client = Client( + name='ios', client_id='client', client_secret='secret', + _redirect_uris='http://localhost/authorized', + ) + + db.session.add(User(username='foo')) + db.session.add(oauth_client) + + self.oauth_client = oauth_client + + def test_get_token(self): + rv = self.client.post('/oauth/token', data={ + 'grant_type': 'client_credentials', + 'client_id': self.oauth_client.client_id, + 'client_secret': self.oauth_client.client_secret, + }) + assert b'access_token' in rv.data + + +class TestSQLAlchemyProvider(TestDefaultProvider): + def create_server(self): + create_server(self.app, sqlalchemy_provider(self.app)) + + +class TestCacheProvider(TestDefaultProvider): + def create_server(self): + create_server(self.app, cache_provider(self.app)) From 77b248c18bf1edfe6d36fb8c1ca2fc96258dacb2 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Apr 2015 15:28:23 +0800 Subject: [PATCH 180/279] Add test for refresh token --- tests/test_oauth2/test_refresh.py | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/test_oauth2/test_refresh.py diff --git a/tests/test_oauth2/test_refresh.py b/tests/test_oauth2/test_refresh.py new file mode 100644 index 00000000..96b56160 --- /dev/null +++ b/tests/test_oauth2/test_refresh.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +from .base import TestCase +from .base import create_server, sqlalchemy_provider, cache_provider +from .base import db, Client, User, Token + + +class TestDefaultProvider(TestCase): + def create_server(self): + create_server(self.app) + + def prepare_data(self): + self.create_server() + + oauth_client = Client( + name='ios', client_id='client', client_secret='secret', + _redirect_uris='http://localhost/authorized', + ) + + db.session.add(User(username='foo')) + db.session.add(oauth_client) + db.session.commit() + + self.oauth_client = oauth_client + + def test_get_token(self): + user = User.query.first() + token = Token( + user_id=user.id, + client_id=self.oauth_client.client_id, + access_token='foo', + refresh_token='bar', + expires_in=1000, + ) + db.session.add(token) + db.session.commit() + + rv = self.client.post('/oauth/token', data={ + 'grant_type': 'refresh_token', + 'refresh_token': token.refresh_token, + 'client_id': self.oauth_client.client_id, + 'client_secret': self.oauth_client.client_secret, + }) + assert b'access_token' in rv.data + + +class TestSQLAlchemyProvider(TestDefaultProvider): + def create_server(self): + create_server(self.app, sqlalchemy_provider(self.app)) + + +class TestCacheProvider(TestDefaultProvider): + def create_server(self): + create_server(self.app, cache_provider(self.app)) From 048deab8324b3d633521d072df9d4314b32a35ca Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Apr 2015 15:30:18 +0800 Subject: [PATCH 181/279] Fix missing commit --- tests/test_oauth2/test_client_credential.py | 1 + tests/test_oauth2/test_code.py | 1 + tests/test_oauth2/test_implicit.py | 1 + tests/test_oauth2/test_password.py | 1 + tests/test_oauth2/test_refresh.py | 7 +++++++ 5 files changed, 11 insertions(+) diff --git a/tests/test_oauth2/test_client_credential.py b/tests/test_oauth2/test_client_credential.py index bf11e2e7..231dcb71 100644 --- a/tests/test_oauth2/test_client_credential.py +++ b/tests/test_oauth2/test_client_credential.py @@ -19,6 +19,7 @@ def prepare_data(self): db.session.add(User(username='foo')) db.session.add(oauth_client) + db.session.commit() self.oauth_client = oauth_client diff --git a/tests/test_oauth2/test_code.py b/tests/test_oauth2/test_code.py index 617cf189..fd371179 100644 --- a/tests/test_oauth2/test_code.py +++ b/tests/test_oauth2/test_code.py @@ -20,6 +20,7 @@ def prepare_data(self): db.session.add(User(username='foo')) db.session.add(oauth_client) + db.session.commit() self.oauth_client = oauth_client self.authorize_url = ( diff --git a/tests/test_oauth2/test_implicit.py b/tests/test_oauth2/test_implicit.py index 919ef907..f464d862 100644 --- a/tests/test_oauth2/test_implicit.py +++ b/tests/test_oauth2/test_implicit.py @@ -19,6 +19,7 @@ def prepare_data(self): db.session.add(User(username='foo')) db.session.add(oauth_client) + db.session.commit() self.oauth_client = oauth_client diff --git a/tests/test_oauth2/test_password.py b/tests/test_oauth2/test_password.py index 20c11b57..1c26a8dd 100644 --- a/tests/test_oauth2/test_password.py +++ b/tests/test_oauth2/test_password.py @@ -19,6 +19,7 @@ def prepare_data(self): db.session.add(User(username='foo')) db.session.add(oauth_client) + db.session.commit() self.oauth_client = oauth_client diff --git a/tests/test_oauth2/test_refresh.py b/tests/test_oauth2/test_refresh.py index 96b56160..a04627bc 100644 --- a/tests/test_oauth2/test_refresh.py +++ b/tests/test_oauth2/test_refresh.py @@ -35,6 +35,13 @@ def test_get_token(self): db.session.add(token) db.session.commit() + rv = self.client.post('/oauth/token', data={ + 'grant_type': 'refresh_token', + 'refresh_token': token.refresh_token, + 'client_id': self.oauth_client.client_id, + }) + assert b'error' in rv.data + rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': token.refresh_token, From ec29a4394f7c3f7689544d31eed3f496d0447532 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Apr 2015 16:16:38 +0800 Subject: [PATCH 182/279] Add coverage configuration. omit contrib client now --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..79ceac56 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[report] +omit = flask_oauthlib/contrib/client/* From 2e5e1c4602124d854f88bb88fa5d888941f9ae0d Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Mon, 13 Apr 2015 13:02:04 +0700 Subject: [PATCH 183/279] Fix facebook example with GET access token method --- example/facebook.py | 1 + 1 file changed, 1 insertion(+) diff --git a/example/facebook.py b/example/facebook.py index 2d4473f8..e7d8ab26 100644 --- a/example/facebook.py +++ b/example/facebook.py @@ -19,6 +19,7 @@ base_url='https://graph.facebook.com/', request_token_url=None, access_token_url='/oauth/access_token', + access_token_method='GET', authorize_url='https://www.facebook.com/dialog/oauth' ) From 1454a6bcbb5248715363b416317a8c63315cfe89 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Thu, 30 Apr 2015 23:42:37 +0200 Subject: [PATCH 184/279] Fix compliance_fixes not being correctly checked --- flask_oauthlib/contrib/client/application.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py index f792714e..91c0d667 100644 --- a/flask_oauthlib/contrib/client/application.py +++ b/flask_oauthlib/contrib/client/application.py @@ -307,11 +307,12 @@ def make_oauth_session(self, **kwargs): # patches session compliance_fixes = self.compliance_fixes - if compliance_fixes.startswith('.'): - compliance_fixes = \ - 'requests_oauthlib.compliance_fixes' + compliance_fixes - apply_fixes = import_string(compliance_fixes) - oauth = apply_fixes(oauth) + if compliance_fixes is not None: + if compliance_fixes.startswith('.'): + compliance_fixes = \ + 'requests_oauthlib.compliance_fixes' + compliance_fixes + apply_fixes = import_string(compliance_fixes) + oauth = apply_fixes(oauth) return oauth From d8966f15d588c436ce6ee70b030cbd53aed68e2d Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Thu, 21 May 2015 00:47:17 +0800 Subject: [PATCH 185/279] Contrib-OAuth1: refactor the implementation of OAuth 1.0a support. Storing request token and request token secret in cookie is nonstandard and insecure. --- flask_oauthlib/contrib/client/application.py | 37 +++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py index 91c0d667..36043f4e 100644 --- a/flask_oauthlib/contrib/client/application.py +++ b/flask_oauthlib/contrib/client/application.py @@ -15,6 +15,7 @@ from flask import current_app, redirect, request from requests_oauthlib import OAuth1Session, OAuth2Session +from requests_oauthlib.oauth1_session import TokenMissing from oauthlib.oauth2.rfc6749.errors import MissingCodeError from werkzeug.utils import import_string @@ -155,8 +156,6 @@ class OAuth1Application(BaseApplication): session_class = OAuth1Session - _session_request_token = WebSessionData('req_token') - def make_client(self, token): """Creates a client with specific access token pair. @@ -175,15 +174,16 @@ def make_client(self, token): resource_owner_secret=access_token_secret) def authorize(self, callback_uri, code=302): + # TODO add support for oauth_callback=oob (out-of-band) here + # http://tools.ietf.org/html/rfc5849#section-2.1 oauth = self.make_oauth_session(callback_uri=callback_uri) # fetches request token - response = oauth.fetch_request_token(self.request_token_url) - request_token = response['oauth_token'] - request_token_secret = response['oauth_token_secret'] - - # stores request token and callback uri - self._session_request_token = (request_token, request_token_secret) + oauth.fetch_request_token(self.request_token_url) + # TODO send signal and pass token here + # http://flask.pocoo.org/docs/0.10/signals/ + # TODO check oauth_callback_confirmed here + # http://tools.ietf.org/html/rfc5849#section-2.1 # redirects to third-part URL authorization_url = oauth.authorization_url(http://github.com/self.authorization_url) @@ -194,26 +194,13 @@ def authorized_response(self): # obtains verifier try: - response = oauth.parse_authorization_response(request.url) - except ValueError as e: - if 'denied' not in repr(e).split("'"): - raise + oauth.parse_authorization_response(request.url) + except TokenMissing: return # authorization denied - verifier = response['oauth_verifier'] - - # restores request token from session - if not self._session_request_token: - return - request_token, request_token_secret = self._session_request_token - del self._session_request_token # obtains access token - oauth = self.make_oauth_session( - resource_owner_key=request_token, - resource_owner_secret=request_token_secret, - verifier=verifier) - oauth_tokens = oauth.fetch_access_token(self.access_token_url) - return OAuth1Response(oauth_tokens) + token = oauth.fetch_access_token(self.access_token_url) + return OAuth1Response(token) def make_oauth_session(self, **kwargs): oauth = self.session_class( From 7909721d01b402983412462302ec0ac28f83371c Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Thu, 21 May 2015 00:55:54 +0800 Subject: [PATCH 186/279] Contrib-OAuth1: use the key defined in token responses. --- flask_oauthlib/contrib/client/application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py index 36043f4e..c933d70e 100644 --- a/flask_oauthlib/contrib/client/application.py +++ b/flask_oauthlib/contrib/client/application.py @@ -165,8 +165,8 @@ def make_client(self, token): object. """ if isinstance(token, dict): - access_token = token['token'] - access_token_secret = token['token_secret'] + access_token = token['oauth_token'] + access_token_secret = token['oauth_token_secret'] else: access_token, access_token_secret = token return self.make_oauth_session( From ad2fc37e5e2898930046328929e8ed881e3b2ffb Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Thu, 21 May 2015 01:35:08 +0800 Subject: [PATCH 187/279] Contrib-OAuth1: send signal "request_token_fetched" by blinker. --- flask_oauthlib/contrib/client/application.py | 6 +++--- flask_oauthlib/contrib/client/signals.py | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 flask_oauthlib/contrib/client/signals.py diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py index c933d70e..43936932 100644 --- a/flask_oauthlib/contrib/client/application.py +++ b/flask_oauthlib/contrib/client/application.py @@ -22,6 +22,7 @@ from .descriptor import OAuthProperty, WebSessionData from .structure import OAuth1Response, OAuth2Response from .exceptions import AccessTokenNotFound +from .signals import request_token_fetched __all__ = ['OAuth1Application', 'OAuth2Application'] @@ -179,9 +180,8 @@ def authorize(self, callback_uri, code=302): oauth = self.make_oauth_session(callback_uri=callback_uri) # fetches request token - oauth.fetch_request_token(self.request_token_url) - # TODO send signal and pass token here - # http://flask.pocoo.org/docs/0.10/signals/ + token = oauth.fetch_request_token(self.request_token_url) + request_token_fetched.send(self, response=OAuth1Response(token)) # TODO check oauth_callback_confirmed here # http://tools.ietf.org/html/rfc5849#section-2.1 diff --git a/flask_oauthlib/contrib/client/signals.py b/flask_oauthlib/contrib/client/signals.py new file mode 100644 index 00000000..13df6e33 --- /dev/null +++ b/flask_oauthlib/contrib/client/signals.py @@ -0,0 +1,6 @@ +from flask.signals import Namespace + +__all__ = ['request_token_fetched'] + +_signals = Namespace() +request_token_fetched = _signals.signal('request-token-fetched') From c1f24350257ae241f90b56c918629e0570859d6d Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Thu, 21 May 2015 01:36:18 +0800 Subject: [PATCH 188/279] Require request-oauthlib>0.5 because "TokenMissing" is broken in 0.4. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e1afc16d..afc3e455 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ def fread(filename): install_requires=[ 'Flask', 'oauthlib>=0.6.2', - 'requests-oauthlib>=0.4.1', + 'requests-oauthlib>=0.5.0', ], tests_require=['nose', 'Flask-SQLAlchemy', 'mock'], test_suite='nose.collector', From 236adc092f8d2de0bf2765533ebbf4002f614039 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 21 May 2015 10:05:08 +0800 Subject: [PATCH 189/279] request data can be None --- flask_oauthlib/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 80e028e3..deca9ef1 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -464,8 +464,12 @@ def request(self, url, data=None, headers=None, format='urlencoded', # change the uri, headers, or body. uri, headers, body = self.pre_request(uri, headers, body) + if body: + data = to_bytes(body, self.encoding) + else: + data = None resp, content = self.http_request( - uri, headers, data=to_bytes(body, self.encoding), method=method + uri, headers, data=data, method=method ) return OAuthResponse(resp, content, self.content_type) From 1b995acedf88c6eb821917b6bb06176ab010eb6e Mon Sep 17 00:00:00 2001 From: huxuan Date: Sat, 30 May 2015 13:56:59 +0800 Subject: [PATCH 190/279] Typo fixed in CONTRIBUTING.rst. --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8f060805..71af73b4 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -36,7 +36,7 @@ to guarantee functionality and keep all code written in a good style. You should follow the code style. Here are some tips to make things simple: * When you cloned this repo, run ``pip install -r requirements.txt`` -* Check the code style with ``make link`` +* Check the code style with ``make lint`` * Check the test with ``make test`` * Check the test coverage with ``make coverage`` From dc55998a41f611e70da266212607a06af1f9dabc Mon Sep 17 00:00:00 2001 From: Jacques Dafflon Date: Wed, 3 Jun 2015 14:10:00 +0200 Subject: [PATCH 191/279] Remove incorrect handling of scope The contrib client joins a list of scope with a comma which is wrong. RFC6749 section 3.3 specifies the scopes must be "expressed as a list of space-delimited, case-sensitive strings.". Processing the scope at this point is also not necessary as the scope are passed to requests-oauthlib and then oauthlib which can handle a Python list just fine. --- flask_oauthlib/contrib/client/application.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/flask_oauthlib/contrib/client/application.py b/flask_oauthlib/contrib/client/application.py index 43936932..0694ace1 100644 --- a/flask_oauthlib/contrib/client/application.py +++ b/flask_oauthlib/contrib/client/application.py @@ -273,10 +273,7 @@ def authorized_response(self): return OAuth2Response(token) def make_oauth_session(self, **kwargs): - # joins scope into unicode kwargs.setdefault('scope', self.scope) - if kwargs['scope']: - kwargs['scope'] = u','.join(kwargs['scope']) # configures automatic token refresh if possible if self.refresh_token_url: From 0f22de65dae6f0392758e12fb08336892020e197 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Mon, 8 Jun 2015 04:23:25 +0800 Subject: [PATCH 192/279] Use shields.io instead of pypip.in --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 925531b4..4bb8b605 100644 --- a/README.rst +++ b/README.rst @@ -1,10 +1,10 @@ Flask-OAuthlib ============== -.. image:: https://pypip.in/wheel/flask-oauthlib/badge.svg?style=flat +.. image:: https://img.shields.io/pypi/wheel/flask-oauthlib.svg?style=flat :target: https://pypi.python.org/pypi/flask-OAuthlib/ :alt: Wheel Status -.. image:: https://pypip.in/version/flask-oauthlib/badge.svg?style=flat +.. image:: https://img.shields.io/pypi/v/flask-oauthlib.svg?style=flat :target: https://pypi.python.org/pypi/flask-oauthlib/ :alt: Latest Version .. image:: https://travis-ci.org/lepture/flask-oauthlib.svg?branch=master From 293f3579767f13d3c7e21061af7b33965d86f8cd Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Mon, 15 Jun 2015 13:54:57 -0500 Subject: [PATCH 193/279] fix some of the English in index and intro --- docs/index.rst | 6 ++++-- docs/intro.rst | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index ea88aedc..a009d632 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,11 +3,13 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +.. _oauthlib: https://github.com/idan/oauthlib + Flask-OAuthlib ============== -Flask-OAuthlib is designed as a replacement for Flask-OAuth. It depends -on the oauthlib module. +Flask-OAuthlib is designed to be a replacement for Flask-OAuth. It depends on +oauthlib_. The client part of Flask-OAuthlib shares the same API as Flask-OAuth, which is pretty and simple. diff --git a/docs/intro.rst b/docs/intro.rst index 3d2371d4..b25d055f 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -3,26 +3,26 @@ Introduction ============ -Flask-OAuthlib is designed as a replacement for Flask-OAuth. It depends on -the oauthlib module. - +Flask-OAuthlib is designed to be a replacement for Flask-OAuth. It depends on +oauthlib_. Why --- -The original `Flask-OAuth`_ is lack of maintenance, and oauthlib_ is a -promising replacement for oauth2. +The original `Flask-OAuth`_ suffers from lack of maintenance, and oauthlib_ is a +promising replacement for `python-oauth2`_. .. _`Flask-OAuth`: http://pythonhosted.org/Flask-OAuth/ .. _oauthlib: https://github.com/idan/oauthlib +.. _`python-oauth2`: https://pypi.python.org/pypi/oauth2/ -There are lots of non-standard services that claim themself as oauth providers, -but the API are always broken. When rewrite a oauth extension for flask, -I do take them into consideration, Flask-OAuthlib does support those -non-standard services. +There are lots of non-standard services that claim they are oauth providers, but +their APIs are always broken. While rewriteing an oauth extension for Flask, I +took them into consideration. Flask-OAuthlib does support these non-standard +services. -Flask-OAuthlib also provide the solution for creating an oauth service. -It does support oauth1 and oauth2 (with Bearer Token) server now. +Flask-OAuthlib also provides the solution for creating an oauth service. It +supports both oauth1 and oauth2 (with Bearer Token). import this ----------- From 09a127b470bdf4552d90300b8e5f1dcd38f12e1b Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Mon, 15 Jun 2015 13:55:22 -0500 Subject: [PATCH 194/279] taking out easy_install, it's time --- docs/install.rst | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index c583f699..5c8b9bfb 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -4,23 +4,15 @@ Installation ============ This part of the documentation covers the installation of Flask-OAuthlib. -The first step to using any software package is getting it properly installed. -Distribute & Pip ----------------- +Pip +--- Installing Flask-OAuthlib is simple with `pip `_:: $ pip install Flask-OAuthlib -or, with `easy_install `_:: - - $ easy_install Flask-OAuthlib - -But, you really `shouldn't do that `_. - - Cheeseshop Mirror ----------------- From 094907c61b5b3d383a16116ca8c926c20d7219a8 Mon Sep 17 00:00:00 2001 From: Isaac Cook Date: Wed, 17 Jun 2015 16:32:50 -0500 Subject: [PATCH 195/279] Fix expires_in not returning to client in example When popping the "expires_in" from client this causes the client to not recieve that information in the response, when it was likely intended that they should. --- docs/oauth2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/oauth2.rst b/docs/oauth2.rst index cd95a125..05144069 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -310,7 +310,7 @@ and accessing resource flow. They are implemented with decorators as follows:: for t in toks: db.session.delete(t) - expires_in = token.pop('expires_in') + expires_in = token.get('expires_in') expires = datetime.utcnow() + timedelta(seconds=expires_in) tok = Token( From d03c6742f4cb3c4aeceba461a4c7e2aa17e12f0b Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 31 Jul 2015 10:56:23 +0800 Subject: [PATCH 196/279] Pin requirements --- requirements.txt | 10 +++++----- tests/test_oauth2/test_code.py | 4 ++-- tox.ini | 4 +--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5fc83230..858cb3d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -Flask -Mock -oauthlib -requests-oauthlib -Flask-SQLAlchemy +Flask==0.10.1 +mock==1.3.0 +oauthlib==0.7.2 +requests-oauthlib==0.5.0 +Flask-SQLAlchemy==2.0 diff --git a/tests/test_oauth2/test_code.py b/tests/test_oauth2/test_code.py index fd371179..758a2023 100644 --- a/tests/test_oauth2/test_code.py +++ b/tests/test_oauth2/test_code.py @@ -29,10 +29,10 @@ def prepare_data(self): def test_get_authorize(self): rv = self.client.get('/oauth/authorize') - assert 'invalid_client_id' in rv.location + assert 'client_id' in rv.location rv = self.client.get('/oauth/authorize?client_id=no') - assert 'invalid_client_id' in rv.location + assert 'client_id' in rv.location url = '/oauth/authorize?client_id=%s' % self.oauth_client.client_id rv = self.client.get(url) diff --git a/tox.ini b/tox.ini index c253bde6..d0d13b5c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,6 @@ envlist = py26,py27,py33,py34,pypy [testenv] deps = - git+https://github.com/idan/oauthlib.git#egg=oauthlib nose - Mock - Flask-SQLAlchemy + -rrequirements.txt commands = nosetests -s From 16e346a334bbafe97bd5ba1abf06177ec676a6a2 Mon Sep 17 00:00:00 2001 From: Alexander Putilin Date: Fri, 31 Jul 2015 19:41:08 +0300 Subject: [PATCH 197/279] Add reddit example --- example/reddit.py | 115 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 example/reddit.py diff --git a/example/reddit.py b/example/reddit.py new file mode 100644 index 00000000..96f2d72c --- /dev/null +++ b/example/reddit.py @@ -0,0 +1,115 @@ +from flask import Flask, redirect, url_for, session, request +from flask_oauthlib.client import OAuth, OAuthException, OAuthRemoteApp, parse_response +from flask_oauthlib.utils import to_bytes +import uuid +import base64 +import time + +REDDIT_APP_ID = '6WnQXb-elQ3DLw' +REDDIT_APP_SECRET = 'KzQickJEBxNHmt5bpO_HmSiupTw' +# Reddit requires you to set nice User-Agent containing your username +REDDIT_USER_AGENT = 'flask-oauthlib testing by /u/' + +app = Flask(__name__) +app.debug = True +app.secret_key = 'development' +oauth = OAuth(app) + + +class RedditOAuthRemoteApp(OAuthRemoteApp): + def __init__(self, *args, **kwargs): + super(RedditOAuthRemoteApp, self).__init__(*args, **kwargs) + + def handle_oauth2_response(self): + if self.access_token_method != 'POST': + raise OAuthException( + 'Unsupported access_token_method: %s' % + self.access_token_method + ) + + client = self.make_client() + remote_args = { + 'code': request.args.get('code'), + 'client_secret': self.consumer_secret, + 'redirect_uri': session.get('%s_oauthredir' % self.name) + } + remote_args.update(self.access_token_params) + + reddit_basic_auth = base64.encodestring('%s:%s' % (REDDIT_APP_ID, REDDIT_APP_SECRET)).replace('\n', '') + body = client.prepare_request_body(**remote_args) + while True: + resp, content = self.http_request( + self.expand_url(http://github.com/self.access_token_url), + headers={'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Basic %s' % reddit_basic_auth, + 'User-Agent': REDDIT_USER_AGENT}, + data=to_bytes(body, self.encoding), + method=self.access_token_method, + ) + # Reddit API is rate-limited, so if we get 429, we need to retry + if resp.code != 429: + break + time.sleep(1) + + data = parse_response(resp, content, content_type=self.content_type) + if resp.code not in (200, 201): + raise OAuthException( + 'Invalid response from %s' % self.name, + type='invalid_response', data=data + ) + return data + +reddit = RedditOAuthRemoteApp( + oauth, + 'reddit', + consumer_key=REDDIT_APP_ID, + consumer_secret=REDDIT_APP_SECRET, + request_token_params={'scope': 'identity'}, + base_url='https://oauth.reddit.com/api/v1/', + request_token_url=None, + access_token_url='https://www.reddit.com/api/v1/access_token', + access_token_method='POST', + authorize_url='https://www.reddit.com/api/v1/authorize' +) + +oauth.remote_apps['reddit'] = reddit + + +@app.route('/') +def index(): + return redirect(url_for('login')) + + +@app.route('/login') +def login(): + callback = url_for('reddit_authorized', _external=True) + return reddit.authorize(callback=callback, state=uuid.uuid4()) + + +@app.route('/login/authorized') +def reddit_authorized(): + resp = reddit.authorized_response() + if isinstance(resp, OAuthException): + print resp.data + + if resp is None: + return 'Access denied: error=%s' % request.args['error'], + print resp + session['reddit_oauth_token'] = (resp['access_token'], '') + + # This request may fail(429 Too Many Requests) + # If you plan to use API heavily(and not just for auth), + # it may be better to use PRAW: https://github.com/praw-dev/praw + me = reddit.get('me') + print me.data + return 'Logged in as name=%s link_karma=%s comment_karma=%s' % \ + (me.data['name'], me.data['link_karma'], me.data['comment_karma']) + + +@reddit.tokengetter +def get_reddit_oauth_token(): + return session.get('reddit_oauth_token') + + +if __name__ == '__main__': + app.run() From cf24784fe47e434edf6e22f71b95251ffdc921da Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 7 Aug 2015 08:36:01 +0800 Subject: [PATCH 198/279] Fix typo in oauth1 provider #232 --- flask_oauthlib/provider/oauth1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index 7886a6be..eb5fdba0 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -576,7 +576,7 @@ def client_key_length(self): ) @property - def reqeust_token_length(self): + def request_token_length(self): return self._config.get( 'OAUTH1_PROVIDER_KEY_LENGTH', (20, 30) From d996c1abdf8ed1d5b59342ed37fb8c61fa6331bd Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 29 Sep 2015 12:23:58 +0800 Subject: [PATCH 199/279] require scopes can be multiple. #181 --- flask_oauthlib/provider/oauth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index f14361bf..fa5d37a1 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -757,7 +757,7 @@ def validate_bearer_token(self, token, scopes, request): return False # validate scopes - if not set(tok.scopes).issuperset(set(scopes)): + if scopes and not set(tok.scopes) & set(scopes): msg = 'Bearer token scope not valid.' request.error_message = msg log.debug(msg) From 2811c79be9540cd984d3651243f514121ceb70f0 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 3 Nov 2015 11:54:42 +0800 Subject: [PATCH 200/279] Version bump 0.9.2 --- CHANGES.rst | 10 ++++++++++ flask_oauthlib/__init__.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d8a1adda..0d1afde9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,16 @@ Changelog Here you can see the full list of changes between each Flask-OAuthlib release. +Version 0.9.2 +------------- + +Released on Nov 3, 2015 + +- Bugfix in client parse_response when body is none. +- Update contrib client by @tonyseek +- Typo fix for OAuth1 provider +- Fix OAuth2 provider on non credential clients by @Fleurer + Version 0.9.1 ------------- diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index 73a182d3..a3d7f6f1 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -7,11 +7,11 @@ remote OAuth enabled applications, and also helps you creating your own OAuth servers. - :copyright: (c) 2013 - 2014 by Hsiaoming Yang. + :copyright: (c) 2013 - 2015 by Hsiaoming Yang. :license: BSD, see LICENSE for more details. """ -__version__ = "0.9.1" +__version__ = "0.9.2" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' From 9526b53637151ffada5c3c2e88dac5d0aeb0b824 Mon Sep 17 00:00:00 2001 From: Marvin Reimer Date: Mon, 27 Jul 2015 17:37:46 +0200 Subject: [PATCH 201/279] upgrade Google scopes Upgrade deprecated Google scopes according to https://developers.google.com/identity/sign-in/auth-migration#oauth2login https://developers.google.com/identity/sign-in/auth-migration#email --- example/google.py | 2 +- flask_oauthlib/contrib/apps.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/google.py b/example/google.py index 60ad174a..1aabf055 100644 --- a/example/google.py +++ b/example/google.py @@ -22,7 +22,7 @@ consumer_key=app.config.get('GOOGLE_ID'), consumer_secret=app.config.get('GOOGLE_SECRET'), request_token_params={ - 'scope': 'https://www.googleapis.com/auth/userinfo.email' + 'scope': 'email' }, base_url='https://www.googleapis.com/oauth2/v1/', request_token_url=None, diff --git a/flask_oauthlib/contrib/apps.py b/flask_oauthlib/contrib/apps.py index ecb3dece..09b67e88 100644 --- a/flask_oauthlib/contrib/apps.py +++ b/flask_oauthlib/contrib/apps.py @@ -158,10 +158,10 @@ def processor(**kwargs): The OAuth app for Google API. :param scope: optional. - default: ``['https://www.googleapis.com/auth/userinfo.email']``. + default: ``['email']``. """) google.kwargs_processor(make_scope_processor( - 'https://www.googleapis.com/auth/userinfo.email')) + 'email')) twitter = RemoteAppFactory('twitter', { From 693e4903bab511f681de7b951f9d3e3adda625c4 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 13 Nov 2015 09:32:21 +0800 Subject: [PATCH 202/279] Add Security Reporting Section. #185 --- README.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.rst b/README.rst index 4bb8b605..c6628212 100644 --- a/README.rst +++ b/README.rst @@ -39,6 +39,13 @@ And request more features at `github issues`_. .. _`github issues`: https://github.com/lepture/flask-oauthlib/issues +Security Reporting +------------------ + +If you found security bugs which can not be public, send me email at `me@lepture.com`. +Attachment with patch is welcome. + + Installation ------------ From 879d8d09525eebf61c5752c1f4f7141cf98db81b Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 13 Nov 2015 09:36:02 +0800 Subject: [PATCH 203/279] Don't catch OAuthException in authorized_response. #191 --- flask_oauthlib/client.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index deca9ef1..51b3fd7d 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -664,15 +664,9 @@ def handle_unknown_response(self): def authorized_response(self): """Handles authorization response smartly.""" if 'oauth_verifier' in request.args: - try: - data = self.handle_oauth1_response() - except OAuthException as e: - data = e + data = self.handle_oauth1_response() elif 'code' in request.args: - try: - data = self.handle_oauth2_response() - except OAuthException as e: - data = e + data = self.handle_oauth2_response() else: data = self.handle_unknown_response() From 1bd622322f0117c08d7bff90e078036ab5603ed1 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 13 Nov 2015 09:57:27 +0800 Subject: [PATCH 204/279] Remove useless test case --- tests/oauth2/test_oauth2.py | 46 ------------------------------------- 1 file changed, 46 deletions(-) diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index dbcb5809..9532c20c 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -146,16 +146,6 @@ def test_get_client(self): rv = self.client.get("/client") assert b'dev' in rv.data - def test_invalid_client_id(self): - authorize_url = ( - '/oauth/authorize?response_type=code&client_id=confidential' - '&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fauthorized' - '&scope=email' - ) - rv = self.client.post(authorize_url, data={'confirm': 'yes'}) - rv = self.client.get(clean_url(http://github.com/rv.location)) - assert b'Invalid' in rv.data - def test_invalid_response_type(self): authorize_url = ( '/oauth/authorize?response_type=invalid&client_id=dev' @@ -190,42 +180,6 @@ def create_oauth_provider(self, app): return sqlalchemy_provider(app) -class TestPasswordAuth(OAuthSuite): - - def create_oauth_provider(self, app): - return default_provider(app) - - def test_get_access_token(self): - url = ('/oauth/token?grant_type=password&state=foo' - '&scope=email+address&username=admin&password=admin') - rv = self.client.get(url, headers={ - 'Authorization': 'Basic %s' % auth_code, - }) - assert b'access_token' in rv.data - assert b'state' in rv.data - - def test_invalid_user_credentials(self): - url = ('/oauth/token?grant_type=password&state=foo' - '&scope=email+address&username=fake&password=admin') - rv = self.client.get(url, headers={ - 'Authorization': 'Basic %s' % auth_code, - }) - - assert b'Invalid credentials given' in rv.data - - -class TestPasswordAuthCached(TestPasswordAuth): - - def create_oauth_provider(self, app): - return cache_provider(app) - - -class TestPasswordAuthSQLAlchemy(TestPasswordAuth): - - def create_oauth_provider(self, app): - return sqlalchemy_provider(app) - - class TestRefreshToken(OAuthSuite): def create_oauth_provider(self, app): From f0454d1baf4eee52e77551ed8f79b747729d90b3 Mon Sep 17 00:00:00 2001 From: Jakob Auer Date: Thu, 26 Nov 2015 11:38:30 +0100 Subject: [PATCH 205/279] Fix a bug in validate_grant_type Grant type is allowed if it is part of the 'allowed_grant_types' of the selected client or if it is one of the default grant types --- flask_oauthlib/provider/oauth2.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index fa5d37a1..3c8f9de7 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -823,13 +823,15 @@ def validate_grant_type(self, client_id, grant_type, client, request, 'authorization_code', 'password', 'client_credentials', 'refresh_token', ) - - if grant_type not in default_grant_types: - return False - - if hasattr(client, 'allowed_grant_types') and \ - grant_type not in client.allowed_grant_types: - return False + + # Grant type is allowed if it is part of the 'allowed_grant_types' + # of the selected client or if it is one of the default grant types + if hasattr(client, 'allowed_grant_types'): + if grant_type not in client.allowed_grant_types: + return False + else: + if grant_type not in default_grant_types: + return False if grant_type == 'client_credentials': if not hasattr(client, 'user'): From 8d4faabf17f2fa99ab5f927d3f94fbb1641ab914 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 3 Dec 2015 11:52:13 +0800 Subject: [PATCH 206/279] Revert "Fix for authenticate client" This reverts commit d66849276d37552cdd02473924de875a794edca6. --- flask_oauthlib/provider/oauth2.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 3c8f9de7..204edb56 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -232,6 +232,7 @@ def clientgetter(self, f): - client_id: A random string - client_secret: A random string + - client_type: A string represents if it is `confidential` - redirect_uris: A list of redirect uris - default_redirect_uri: One of the redirect uris - default_scopes: Default scopes of the client @@ -581,10 +582,24 @@ def client_authentication_required(self, request, *args, **kwargs): .. _`Section 4.1.3`: http://tools.ietf.org/html/rfc6749#section-4.1.3 .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 """ - grant_types = ('password', 'authorization_code', 'refresh_token') - return request.grant_type in grant_types + + if request.grant_type == 'password': + client = self._clientgetter(request.client_id) + return (not client) or client.client_type == 'confidential' \ + or client.client_secret + elif request.grant_type == 'authorization_code': + client = self._clientgetter(request.client_id) + return (not client) or client.client_type == 'confidential' + return 'Authorization' in request.headers \ + and request.grant_type == 'refresh_token' def authenticate_client(self, request, *args, **kwargs): + """Authenticate itself in other means. + + Other means means is described in `Section 3.2.1`_. + + .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + """ auth = request.headers.get('Authorization', None) log.debug('Authenticate client %r', auth) if auth: @@ -602,13 +617,15 @@ def authenticate_client(self, request, *args, **kwargs): client = self._clientgetter(client_id) if not client: + log.debug('Authenticate client failed, client not found.') return False + request.client = client + if client.client_secret != client_secret: log.debug('Authenticate client failed, secret not match.') return False - request.client = client log.debug('Authenticate client success.') return True @@ -618,9 +635,8 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): :param client_id: Client ID of the non-confidential client :param request: The Request object passed by oauthlib """ - log.debug('Authenticate client id %r.', client_id) - - client = self._clientgetter(client_id) + log.debug('Authenticate client %r.', client_id) + client = request.client or self._clientgetter(client_id) if not client: log.debug('Authenticate failed, client not found.') return False From 27aa740ff3994e18825c2ed949ce5196ef5be3f8 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Jun 2016 00:06:02 +0900 Subject: [PATCH 207/279] Fix oauth2 confidential detect --- flask_oauthlib/provider/oauth2.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 204edb56..298b4670 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -232,7 +232,7 @@ def clientgetter(self, f): - client_id: A random string - client_secret: A random string - - client_type: A string represents if it is `confidential` + - is_confidential: A bool represents if it is confidential - redirect_uris: A list of redirect uris - default_redirect_uri: One of the redirect uris - default_scopes: Default scopes of the client @@ -583,13 +583,20 @@ def client_authentication_required(self, request, *args, **kwargs): .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 """ + def is_confidential(client): + client_type = getattr(client, 'client_type', None) + if client_type and client_type == 'confidential': + return True + return getattr(client, 'is_confidential', False) + if request.grant_type == 'password': client = self._clientgetter(request.client_id) - return (not client) or client.client_type == 'confidential' \ - or client.client_secret + if not client: + return True + return is_confidential(client) or client.client_secret elif request.grant_type == 'authorization_code': client = self._clientgetter(request.client_id) - return (not client) or client.client_type == 'confidential' + return (not client) or is_confidential(client) return 'Authorization' in request.headers \ and request.grant_type == 'refresh_token' From c762b57c85bd011f5e4daba3f58cadade04247ae Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Jun 2016 00:11:25 +0900 Subject: [PATCH 208/279] Fix test cases --- flask_oauthlib/provider/oauth2.py | 5 ++--- tests/test_oauth2/base.py | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 298b4670..3adb0a86 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -589,7 +589,7 @@ def is_confidential(client): return True return getattr(client, 'is_confidential', False) - if request.grant_type == 'password': + if request.grant_type in ('password', 'refresh_token'): client = self._clientgetter(request.client_id) if not client: return True @@ -597,8 +597,7 @@ def is_confidential(client): elif request.grant_type == 'authorization_code': client = self._clientgetter(request.client_id) return (not client) or is_confidential(client) - return 'Authorization' in request.headers \ - and request.grant_type == 'refresh_token' + return 'Authorization' in request.headers def authenticate_client(self, request, *args, **kwargs): """Authenticate itself in other means. diff --git a/tests/test_oauth2/base.py b/tests/test_oauth2/base.py index 60ba0218..522f4eb7 100644 --- a/tests/test_oauth2/base.py +++ b/tests/test_oauth2/base.py @@ -40,6 +40,10 @@ class Client(db.Model): def user(self): return User.query.get(1) + @property + def is_confidential(self): + return True + @property def redirect_uris(self): if self._redirect_uris: From 216c8c2de735c191ea36d99aa2e07d0c4a5a8fed Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Jun 2016 00:24:40 +0900 Subject: [PATCH 209/279] Update README and travis --- .travis.yml | 3 +-- README.rst | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index d033f2a1..b4a67a58 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,9 @@ language: python python: - - "2.6" - "2.7" - - "3.3" - "3.4" + - "3.5" - "pypy" script: diff --git a/README.rst b/README.rst index c6628212..b61d0dd6 100644 --- a/README.rst +++ b/README.rst @@ -1,10 +1,10 @@ Flask-OAuthlib ============== -.. image:: https://img.shields.io/pypi/wheel/flask-oauthlib.svg?style=flat +.. image:: https://img.shields.io/pypi/wheel/flask-oauthlib.svg :target: https://pypi.python.org/pypi/flask-OAuthlib/ :alt: Wheel Status -.. image:: https://img.shields.io/pypi/v/flask-oauthlib.svg?style=flat +.. image:: https://img.shields.io/pypi/v/flask-oauthlib.svg :target: https://pypi.python.org/pypi/flask-oauthlib/ :alt: Latest Version .. image:: https://travis-ci.org/lepture/flask-oauthlib.svg?branch=master @@ -13,6 +13,9 @@ Flask-OAuthlib .. image:: https://coveralls.io/repos/lepture/flask-oauthlib/badge.svg?branch=master :target: https://coveralls.io/r/lepture/flask-oauthlib :alt: Coverage Status +.. image:: https://img.shields.io/badge/donate-lepture-green.svg + :target: https://lepture.herokuapp.com/?amount=1000&reason=lepture%2Fflask-oauthlib + :alt: Donate leptjure Flask-OAuthlib is an extension to Flask that allows you to interact with From ff462465aa40315a53064bb1166ba032a5f66ac3 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Jun 2016 00:25:03 +0900 Subject: [PATCH 210/279] Fix require authenticate --- flask_oauthlib/provider/oauth2.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 3adb0a86..f7b88182 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -588,16 +588,11 @@ def is_confidential(client): if client_type and client_type == 'confidential': return True return getattr(client, 'is_confidential', False) - - if request.grant_type in ('password', 'refresh_token'): - client = self._clientgetter(request.client_id) - if not client: - return True - return is_confidential(client) or client.client_secret - elif request.grant_type == 'authorization_code': + grant_types = ('password', 'authorization_code', 'refresh_token') + if request.grant_type in grant_types: client = self._clientgetter(request.client_id) return (not client) or is_confidential(client) - return 'Authorization' in request.headers + return False def authenticate_client(self, request, *args, **kwargs): """Authenticate itself in other means. From deb78af12a88ef83e9fdc4c8ba0c6b1f3d63e235 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Jun 2016 00:32:56 +0900 Subject: [PATCH 211/279] Add tests. No need for client secret for non confidential clients --- tests/test_oauth2/base.py | 5 +--- tests/test_oauth2/test_refresh.py | 50 +++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/tests/test_oauth2/base.py b/tests/test_oauth2/base.py index 522f4eb7..bb8a3f82 100644 --- a/tests/test_oauth2/base.py +++ b/tests/test_oauth2/base.py @@ -35,15 +35,12 @@ class Client(db.Model): _redirect_uris = db.Column(db.Text) default_scope = db.Column(db.Text, default='email address') disallow_grant_type = db.Column(db.String(20)) + is_confidential = db.Column(db.Boolean, default=True) @property def user(self): return User.query.get(1) - @property - def is_confidential(self): - return True - @property def redirect_uris(self): if self._redirect_uris: diff --git a/tests/test_oauth2/test_refresh.py b/tests/test_oauth2/test_refresh.py index a04627bc..2a99ea6e 100644 --- a/tests/test_oauth2/test_refresh.py +++ b/tests/test_oauth2/test_refresh.py @@ -12,22 +12,54 @@ def create_server(self): def prepare_data(self): self.create_server() - oauth_client = Client( - name='ios', client_id='client', client_secret='secret', + normal_client = Client( + name='normal_client', + client_id='normal_client', + client_secret='normal_secret', + is_confidential=False, + _redirect_uris='http://localhost/authorized', + ) + + confidential_client = Client( + name='confidential_client', + client_id='confidential_client', + client_secret='confidential_secret', + is_confidential=True, _redirect_uris='http://localhost/authorized', ) db.session.add(User(username='foo')) - db.session.add(oauth_client) + db.session.add(normal_client) + db.session.add(confidential_client) db.session.commit() - self.oauth_client = oauth_client + self.normal_client = normal_client + self.confidential_client = confidential_client + + def test_normal_get_token(self): + user = User.query.first() + token = Token( + user_id=user.id, + client_id=self.normal_client.client_id, + access_token='foo', + refresh_token='bar', + expires_in=1000, + ) + db.session.add(token) + db.session.commit() + + rv = self.client.post('/oauth/token', data={ + 'grant_type': 'refresh_token', + 'refresh_token': token.refresh_token, + 'client_id': self.normal_client.client_id, + }) + assert b'access_token' in rv.data - def test_get_token(self): + def test_confidential_get_token(self): user = User.query.first() token = Token( user_id=user.id, - client_id=self.oauth_client.client_id, + client_id=self.confidential_client.client_id, access_token='foo', refresh_token='bar', expires_in=1000, @@ -38,15 +70,15 @@ def test_get_token(self): rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': token.refresh_token, - 'client_id': self.oauth_client.client_id, + 'client_id': self.confidential_client.client_id, }) assert b'error' in rv.data rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': token.refresh_token, - 'client_id': self.oauth_client.client_id, - 'client_secret': self.oauth_client.client_secret, + 'client_id': self.confidential_client.client_id, + 'client_secret': self.confidential_client.client_secret, }) assert b'access_token' in rv.data From e3a196c2fd3caebf3cc53a22614f3f03d7df28e9 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Jun 2016 00:41:03 +0900 Subject: [PATCH 212/279] Remove test client dependency of tests.oauth2 --- tests/test_client.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 03c80d8c..77c4b48b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -12,7 +12,6 @@ http_urlopen = 'urllib.request.urlopen' from mock import patch -from .oauth2.client import create_client class Response(object): @@ -50,7 +49,18 @@ def test_encode_request_data(): def test_app(): app = Flask(__name__) - create_client(app) + oauth = OAuth(app) + remote = oauth.remote_app( + 'dev', + consumer_key='dev', + consumer_secret='dev', + request_token_params={'scope': 'email'}, + base_url='http://127.0.0.1:5000/api/', + request_token_url=None, + access_token_method='POST', + access_token_url='http://127.0.0.1:5000/oauth/token', + authorize_url='http://127.0.0.1:5000/oauth/authorize' + ) client = app.extensions['oauthlib.client'] assert client.dev.name == 'dev' @@ -68,7 +78,7 @@ def test_parse_xml(): @raises(AttributeError) def test_raise_app(): app = Flask(__name__) - app = create_client(app) + oauth = OAuth(app) client = app.extensions['oauthlib.client'] assert client.demo.name == 'dev' From f4c894338109e335e5175d033060e13489b360a8 Mon Sep 17 00:00:00 2001 From: Adam Chainz Date: Wed, 1 Jun 2016 16:45:16 +0100 Subject: [PATCH 213/279] Convert readthedocs link for their .org -> .io migration for hosted projects (#272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As per [their blog post of the 27th April](https://blog.readthedocs.com/securing-subdomains/) ‘Securing subdomains’: > Starting today, Read the Docs will start hosting projects from subdomains on the domain readthedocs.io, instead of on readthedocs.org. This change addresses some security concerns around site cookies while hosting user generated data on the same domain as our dashboard. Test Plan: Manually visited all the links I’ve modified. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b61d0dd6..924c257f 100644 --- a/README.rst +++ b/README.rst @@ -68,7 +68,7 @@ Additional Notes We keep documentation at `flask-oauthlib@readthedocs`_. -.. _`flask-oauthlib@readthedocs`: https://flask-oauthlib.readthedocs.org +.. _`flask-oauthlib@readthedocs`: https://flask-oauthlib.readthedocs.io If you are only interested in the client part, you can find some examples in the ``example`` directory. From 9db38f801931376b77a11db881b40bdc06abd293 Mon Sep 17 00:00:00 2001 From: Tristan Carel Date: Wed, 1 Jun 2016 17:53:49 +0200 Subject: [PATCH 214/279] OAuthRemoteApp: new access_token_headers parameter (#222) User can specify additional headers given to the access_token_url HTTP request. Rationale: This patch allows me to pass the HTTP 'Authentification' header to Bitbucket that requires basic access authentification for the access token method. --- flask_oauthlib/client.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 51b3fd7d..71c2d0fd 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -208,6 +208,8 @@ class OAuthRemoteApp(object): forward to the access token url :param access_token_method: the HTTP method that should be used for the access_token_url. Default is ``GET`` + :param access_token_headers: additonal headers that should be used for + the access_token_url. :param content_type: force to parse the content with this content_type, usually used when the server didn't return the right content type. @@ -229,6 +231,7 @@ def __init__( request_token_method=None, access_token_params=None, access_token_method=None, + access_token_headers=None, content_type=None, app_key=None, encoding='utf-8', @@ -246,6 +249,7 @@ def __init__( self._request_token_method = request_token_method self._access_token_params = access_token_params self._access_token_method = access_token_method + self._access_token_headers = access_token_headers or {} self._content_type = content_type self._tokengetter = None @@ -469,7 +473,7 @@ def request(self, url, data=None, headers=None, format='urlencoded', else: data = None resp, content = self.http_request( - uri, headers, data=data, method=method + uri, headers, data=to_bytes(body, self.encoding), method=method ) return OAuthResponse(resp, content, self.content_type) @@ -603,6 +607,7 @@ def handle_oauth1_response(self): self.expand_url(http://github.com/self.access_token_url), _encode(self.access_token_method) ) + headers.update(self._access_token_headers) resp, content = self.http_request( uri, headers, to_bytes(data, self.encoding), @@ -627,11 +632,13 @@ def handle_oauth2_response(self): } log.debug('Prepare oauth2 remote args %r', remote_args) remote_args.update(self.access_token_params) + headers = copy(self._access_token_headers) if self.access_token_method == 'POST': + headers.update({'Content-Type': 'application/x-www-form-urlencoded'}) body = client.prepare_request_body(**remote_args) resp, content = self.http_request( self.expand_url(http://github.com/self.access_token_url), - headers={'Content-Type': 'application/x-www-form-urlencoded'}, + headers=headers, data=to_bytes(body, self.encoding), method=self.access_token_method, ) @@ -641,6 +648,7 @@ def handle_oauth2_response(self): url += ('?' in url and '&' or '?') + qs resp, content = self.http_request( url, + headers=headers, method=self.access_token_method, ) else: From fa7d9d68fecc4e7469dfb5a829ed38a4170e11f2 Mon Sep 17 00:00:00 2001 From: Dawei Ma Date: Wed, 1 Jun 2016 23:56:15 +0800 Subject: [PATCH 215/279] [Fix Bug] When library call authenticate_client but client's secret key is null that can cause the program raise a 500 Internal Server Error. (#258) * [Fix Bug] When library call authenticate_client but client's secret key is null that can cause the program raise a 500 Internal Server Error. * [Fix Bug] When library call authenticate_client but client's secret key is null that can cause the program raise a 500 Internal Server Error. --- flask_oauthlib/provider/oauth2.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index f7b88182..258e4c93 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -623,9 +623,11 @@ def authenticate_client(self, request, *args, **kwargs): request.client = client - if client.client_secret != client_secret: - log.debug('Authenticate client failed, secret not match.') - return False + # http://tools.ietf.org/html/rfc6749#section-2 + # The client MAY omit the parameter if the client secret is an empty string. + if hasattr(client, 'client_secret') and client.client_secret != client_secret: + log.debug('Authenticate client failed, secret not match.') + return False log.debug('Authenticate client success.') return True From 63ad953df7985cebd5c6f1a6740962c127482f8b Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Jun 2016 00:57:35 +0900 Subject: [PATCH 216/279] Fix reddit example. Close #249 --- example/reddit.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/example/reddit.py b/example/reddit.py index 96f2d72c..1352480c 100644 --- a/example/reddit.py +++ b/example/reddit.py @@ -90,18 +90,16 @@ def login(): def reddit_authorized(): resp = reddit.authorized_response() if isinstance(resp, OAuthException): - print resp.data + print(resp.data) if resp is None: return 'Access denied: error=%s' % request.args['error'], - print resp session['reddit_oauth_token'] = (resp['access_token'], '') # This request may fail(429 Too Many Requests) # If you plan to use API heavily(and not just for auth), # it may be better to use PRAW: https://github.com/praw-dev/praw me = reddit.get('me') - print me.data return 'Logged in as name=%s link_karma=%s comment_karma=%s' % \ (me.data['name'], me.data['link_karma'], me.data['comment_karma']) From 4cd5642b077d8a05f86d7e0ae779acdcd61a1c02 Mon Sep 17 00:00:00 2001 From: krrr Date: Wed, 1 Jun 2016 23:58:51 +0800 Subject: [PATCH 217/279] Fix qq example for Py3k (#242) --- example/qq.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/example/qq.py b/example/qq.py index a0afd119..1bd9011e 100644 --- a/example/qq.py +++ b/example/qq.py @@ -4,6 +4,7 @@ from flask import Flask, redirect, url_for, session, request, jsonify, Markup from flask_oauthlib.client import OAuth +# get yours at http://connect.qq.com QQ_APP_ID = os.getenv('QQ_APP_ID', '101187283') QQ_APP_KEY = os.getenv('QQ_APP_KEY', '993983549da49e384d03adfead8b2489') @@ -25,13 +26,17 @@ def json_to_dict(x): - '''OAuthResponse class can't not parse the JSON data with content-type - text/html, so we need reload the JSON data manually''' - if x.find('callback') > -1: - pos_lb = x.find('{') - pos_rb = x.find('}') + '''OAuthResponse class can't parse the JSON data with content-type +- text/html and because of a rubbish api, we can't just tell flask-oauthlib to treat it as json.''' + if x.find(b'callback') > -1: + # the rubbish api (https://graph.qq.com/oauth2.0/authorize) is handled here as special case + pos_lb = x.find(b'{') + pos_rb = x.find(b'}') x = x[pos_lb:pos_rb + 1] + try: + if type(x) != str: # Py3k + x = x.decode('utf-8') return json.loads(x, encoding='utf-8') except: return x @@ -60,7 +65,7 @@ def get_user_info(): if 'qq_token' in session: data = update_qq_api_request_data() resp = qq.get('/user/get_user_info', data=data) - return jsonify(status=resp.status, data=resp.data) + return jsonify(status=resp.status, data=json_to_dict(resp.data)) return redirect(url_for('login')) From 51a74915be67e6d11f3a55787969b1d7d2645107 Mon Sep 17 00:00:00 2001 From: faisal burhanudin Date: Wed, 1 Jun 2016 22:59:26 +0700 Subject: [PATCH 218/279] add db on relationship (#241) --- docs/oauth1.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/oauth1.rst b/docs/oauth1.rst index a1b9f082..0b9b56d1 100644 --- a/docs/oauth1.rst +++ b/docs/oauth1.rst @@ -58,7 +58,7 @@ An example of the data model in SQLAlchemy (SQLAlchemy is not required):: # creator of the client, not required user_id = db.Column(db.ForeignKey('user.id')) # required if you need to support client credential - user = relationship('User') + user = db.relationship('User') client_key = db.Column(db.String(40), primary_key=True) client_secret = db.Column(db.String(55), unique=True, index=True, @@ -111,13 +111,13 @@ And the all in one token example:: user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) - user = relationship('User') + user = db.relationship('User') client_key = db.Column( db.String(40), db.ForeignKey('client.client_key'), nullable=False, ) - client = relationship('Client') + client = db.relationship('Client') token = db.Column(db.String(255), index=True, unique=True) secret = db.Column(db.String(255), nullable=False) @@ -161,7 +161,7 @@ Here is an example in SQLAlchemy:: db.String(40), db.ForeignKey('client.client_key'), nullable=False, ) - client = relationship('Client') + client = db.relationship('Client') request_token = db.Column(db.String(50)) access_token = db.Column(db.String(50)) @@ -188,12 +188,12 @@ The implementation in SQLAlchemy:: db.String(40), db.ForeignKey('client.client_key'), nullable=False, ) - client = relationship('Client') + client = db.relationship('Client') user_id = db.Column( db.Integer, db.ForeignKey('user.id'), ) - user = relationship('User') + user = db.relationship('User') token = db.Column(db.String(255)) secret = db.Column(db.String(255)) From 0cabc05073daefbba126eb2c8f2004e4d2a561b7 Mon Sep 17 00:00:00 2001 From: fleuria Date: Thu, 2 Jun 2016 00:08:59 +0800 Subject: [PATCH 219/279] catch Exception in oauth2.py (#223) * catch Exception in oauth2.py if the authorize_url is invalid, the /authorize page will response a 500 Internal Error due to the ValueError from validate_authorization_request did not get caught. * add test on /authorized with invalid redirect uri * move test_invalid_redirect_uri to test_code.py * add test on empty scope in post authorize --- flask_oauthlib/provider/oauth2.py | 19 ++++++++++++++++++- tests/test_oauth2/test_code.py | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 258e4c93..27f8f821 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -18,7 +18,7 @@ from werkzeug.utils import import_string from oauthlib import oauth2 from oauthlib.oauth2 import RequestValidator, Server -from oauthlib.common import to_unicode +from oauthlib.common import to_unicode, add_params_to_uri from ..utils import extract_params, decode_base64, create_response __all__ = ('OAuth2Provider', 'OAuth2RequestValidator') @@ -395,6 +395,11 @@ def decorated(*args, **kwargs): except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e) return redirect(e.in_uri(redirect_uri)) + except Exception as e: + log.warning('Exception caught while processing request, %s.' % e) + return redirect(add_params_to_uri( + self.error_uri, {'error': str(e) } + )) else: redirect_uri = request.values.get( @@ -409,6 +414,12 @@ def decorated(*args, **kwargs): except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e) return redirect(e.in_uri(redirect_uri)) + except Exception as e: + log.warning('Exception caught while processing request, %s.' % e) + return redirect(add_params_to_uri( + self.error_uri, {'error': str(e) } + )) + if not isinstance(rv, bool): # if is a response or redirect @@ -448,6 +459,12 @@ def confirm_authorization_request(self): except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e) return redirect(e.in_uri(redirect_uri or self.error_uri)) + except Exception as e: + log.warning('Exception caught while processing request, %s.' % e) + return redirect(add_params_to_uri( + self.error_uri, {'error': str(e) } + )) + def verify_request(self, scopes): """Verify current request, get the oauth data. diff --git a/tests/test_oauth2/test_code.py b/tests/test_oauth2/test_code.py index 758a2023..421823d8 100644 --- a/tests/test_oauth2/test_code.py +++ b/tests/test_oauth2/test_code.py @@ -50,6 +50,10 @@ def test_post_authorize(self): rv = self.client.post(url, data={'confirm': 'yes'}) assert 'code' in rv.location + url = self.authorize_url + '&scope=' + rv = self.client.post(url, data={'confirm': 'yes'}) + assert 'error=Scopes+must+be+set' in rv.location + def test_invalid_token(self): rv = self.client.get('/oauth/token') assert b'unsupported_grant_type' in rv.data @@ -70,6 +74,16 @@ def test_invalid_token(self): assert b'invalid_client' not in rv.data assert rv.status_code == 401 + def test_invalid_redirect_uri(self): + authorize_url = ( + '/oauth/authorize?response_type=code&client_id=dev' + '&redirect_uri=http://localhost:8000/authorized' + '&scope=invalid' + ) + rv = self.client.get(authorize_url) + assert 'error=' in rv.location + assert 'trying+to+decode+a+non+urlencoded+string' in rv.location + def test_get_token(self): expires = datetime.utcnow() + timedelta(seconds=100) grant = Grant( From 9b407ff3883c2a25fc5053a1df5bf7c8f8115e7c Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 2 Jun 2016 01:16:48 +0900 Subject: [PATCH 220/279] Version bump 0.9.3 --- CHANGES.rst | 10 ++++++++++ flask_oauthlib/__init__.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0d1afde9..472586a5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,16 @@ Changelog Here you can see the full list of changes between each Flask-OAuthlib release. +Version 0.9.3 +------------- + +Released on Jun 2, 2016 + +- Revert the wrong implement of non credential oauth2 require auth +- Catch all exceptions in OAuth2 providers +- Bugfix for examples, docs and other things + + Version 0.9.2 ------------- diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index a3d7f6f1..3e28130e 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -7,11 +7,11 @@ remote OAuth enabled applications, and also helps you creating your own OAuth servers. - :copyright: (c) 2013 - 2015 by Hsiaoming Yang. + :copyright: (c) 2013 - 2016 by Hsiaoming Yang. :license: BSD, see LICENSE for more details. """ -__version__ = "0.9.2" +__version__ = "0.9.3" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' From 9aab7d6c4b21959f8a16fdefb43316c33f2ee935 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 3 Jun 2016 22:25:07 +0900 Subject: [PATCH 221/279] Move donate badge to first --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 924c257f..19b330fe 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,9 @@ Flask-OAuthlib ============== +.. image:: https://img.shields.io/badge/donate-lepture-green.svg + :target: https://lepture.herokuapp.com/?amount=1000&reason=lepture%2Fflask-oauthlib + :alt: Donate lepture .. image:: https://img.shields.io/pypi/wheel/flask-oauthlib.svg :target: https://pypi.python.org/pypi/flask-OAuthlib/ :alt: Wheel Status @@ -13,9 +16,6 @@ Flask-OAuthlib .. image:: https://coveralls.io/repos/lepture/flask-oauthlib/badge.svg?branch=master :target: https://coveralls.io/r/lepture/flask-oauthlib :alt: Coverage Status -.. image:: https://img.shields.io/badge/donate-lepture-green.svg - :target: https://lepture.herokuapp.com/?amount=1000&reason=lepture%2Fflask-oauthlib - :alt: Donate leptjure Flask-OAuthlib is an extension to Flask that allows you to interact with From ec1ddd7df80438fd65705f31c6400ccfe9bd6aac Mon Sep 17 00:00:00 2001 From: Widnyana Putra Date: Tue, 14 Jun 2016 13:14:16 +0700 Subject: [PATCH 222/279] show original error message (#276) (#278) --- example/twitter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/example/twitter.py b/example/twitter.py index f25ca785..0ed61514 100644 --- a/example/twitter.py +++ b/example/twitter.py @@ -59,8 +59,12 @@ def tweet(): resp = twitter.post('statuses/update.json', data={ 'status': status }) + if resp.status == 403: - flash('Your tweet was too long.') + flash("Error: #%d, %s " % ( + resp.data.get('errors')[0].get('code'), + resp.data.get('errors')[0].get('message')) + ) elif resp.status == 401: flash('Authorization error with Twitter.') else: From 663a7ea534e650a409f5fb6410294470fff6b6cb Mon Sep 17 00:00:00 2001 From: Zack Date: Sat, 13 Aug 2016 00:33:16 -0700 Subject: [PATCH 223/279] update deps (#290) --- flask_oauthlib/provider/oauth2.py | 2 +- requirements.txt | 10 +++++----- setup.py | 4 ++-- tests/oauth2/test_oauth2.py | 2 +- tests/test_oauth2/test_code.py | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 27f8f821..72b60cf7 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -960,7 +960,7 @@ def revoke_token(self, token, token_type_hint, request, *args, **kwargs): if not tok: tok = self._tokengetter(refresh_token=token) - if tok and tok.client_id == request.client.client_id: + if tok: request.client_id = tok.client_id request.user = tok.user tok.delete() diff --git a/requirements.txt b/requirements.txt index 858cb3d2..f6331938 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -Flask==0.10.1 -mock==1.3.0 -oauthlib==0.7.2 -requests-oauthlib==0.5.0 -Flask-SQLAlchemy==2.0 +Flask==0.11.1 +mock==2.0.0 +oauthlib==1.1.2 +requests-oauthlib==0.6.2 +Flask-SQLAlchemy==2.1 diff --git a/setup.py b/setup.py index afc3e455..ba565170 100644 --- a/setup.py +++ b/setup.py @@ -43,8 +43,8 @@ def fread(filename): license='BSD', install_requires=[ 'Flask', - 'oauthlib>=0.6.2', - 'requests-oauthlib>=0.5.0', + 'oauthlib>=1.1.2', + 'requests-oauthlib>=0.6.2', ], tests_require=['nose', 'Flask-SQLAlchemy', 'mock'], test_suite='nose.collector', diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index 9532c20c..959315fe 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -69,7 +69,7 @@ def test_login(self): def test_oauth_authorize_invalid_url(http://github.com/self): rv = self.client.get('/oauth/authorize') - assert 'invalid_client_id' in rv.location + assert 'Missing+client_id+parameter.' in rv.location def test_oauth_authorize_valid_url(http://github.com/self): rv = self.client.get(authorize_url) diff --git a/tests/test_oauth2/test_code.py b/tests/test_oauth2/test_code.py index 421823d8..b6d5a9d2 100644 --- a/tests/test_oauth2/test_code.py +++ b/tests/test_oauth2/test_code.py @@ -76,13 +76,13 @@ def test_invalid_token(self): def test_invalid_redirect_uri(self): authorize_url = ( - '/oauth/authorize?response_type=code&client_id=dev' + '/oauth/authorize?response_type=code&client_id=code-client' '&redirect_uri=http://localhost:8000/authorized' '&scope=invalid' ) rv = self.client.get(authorize_url) assert 'error=' in rv.location - assert 'trying+to+decode+a+non+urlencoded+string' in rv.location + assert 'Mismatching+redirect+URI' in rv.location def test_get_token(self): expires = datetime.utcnow() + timedelta(seconds=100) From d1c19460ba9d4b4c4ddfcc7839ec7c46d9d48b29 Mon Sep 17 00:00:00 2001 From: Matthias Hadlich Date: Sat, 13 Aug 2016 09:33:52 +0200 Subject: [PATCH 224/279] Updated OAuth2 example links in oauth2.rst (#288) Adds a link to the github test folder for the OAuth2 server example. The information there appears to be a lot more up to date than those in the mentioned resources. --- docs/oauth2.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/oauth2.rst b/docs/oauth2.rst index 05144069..7681d153 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -490,6 +490,8 @@ The ``request`` has an additional property ``oauth``, it contains at least: Example for OAuth 2 ------------------- -Here is an example of OAuth 2 server: https://github.com/lepture/example-oauth2-server +An examplary server (and client) can be found in the tests folder: https://github.com/lepture/flask-oauthlib/tree/master/tests/oauth2 -Also read this article http://lepture.com/en/2013/create-oauth-server. +Other helpful resources include: + - Another example of an OAuth 2 server: https://github.com/lepture/example-oauth2-server + - An article on how to create an OAuth server: http://lepture.com/en/2013/create-oauth-server. From e1bb478b2015239dd12f1d3ee13fb2627a5a3407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Sat, 13 Aug 2016 09:34:32 +0200 Subject: [PATCH 225/279] Log exception traceback. (#281) This is especially important in an "except Exception" block, as it can catch any exception from any source and for any reason. --- flask_oauthlib/provider/oauth2.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 72b60cf7..ea657d0c 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -390,13 +390,13 @@ def decorated(*args, **kwargs): kwargs['scopes'] = scopes kwargs.update(credentials) except oauth2.FatalClientError as e: - log.debug('Fatal client error %r', e) + log.debug('Fatal client error %r', e, exc_info=True) return redirect(e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: - log.debug('OAuth2Error: %r', e) + log.debug('OAuth2Error: %r', e, exc_info=True) return redirect(e.in_uri(redirect_uri)) except Exception as e: - log.warning('Exception caught while processing request, %s.' % e) + log.warning('Exception caught while processing request, %s.' % e, exc_info=True) return redirect(add_params_to_uri( self.error_uri, {'error': str(e) } )) @@ -409,13 +409,13 @@ def decorated(*args, **kwargs): try: rv = f(*args, **kwargs) except oauth2.FatalClientError as e: - log.debug('Fatal client error %r', e) + log.debug('Fatal client error %r', e, exc_info=True) return redirect(e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: - log.debug('OAuth2Error: %r', e) + log.debug('OAuth2Error: %r', e, exc_info=True) return redirect(e.in_uri(redirect_uri)) except Exception as e: - log.warning('Exception caught while processing request, %s.' % e) + log.warning('Exception caught while processing request, %s.' % e, exc_info=True) return redirect(add_params_to_uri( self.error_uri, {'error': str(e) } )) @@ -454,13 +454,13 @@ def confirm_authorization_request(self): log.debug('Authorization successful.') return create_response(*ret) except oauth2.FatalClientError as e: - log.debug('Fatal client error %r', e) + log.debug('Fatal client error %r', e, exc_info=True) return redirect(e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: - log.debug('OAuth2Error: %r', e) + log.debug('OAuth2Error: %r', e, exc_info=True) return redirect(e.in_uri(redirect_uri or self.error_uri)) except Exception as e: - log.warning('Exception caught while processing request, %s.' % e) + log.warning('Exception caught while processing request, %s.' % e, exc_info=True) return redirect(add_params_to_uri( self.error_uri, {'error': str(e) } )) @@ -627,7 +627,7 @@ def authenticate_client(self, request, *args, **kwargs): client_id = to_unicode(client_id, 'utf-8') client_secret = to_unicode(client_secret, 'utf-8') except Exception as e: - log.debug('Authenticate client failed with exception: %r', e) + log.warning('Authenticate client failed with exception: %r', e, exc_info=True) return False else: client_id = request.client_id From e3081a1db532efbf7f266152a9e4f8432b2f8bc8 Mon Sep 17 00:00:00 2001 From: Andrei Sura Date: Sat, 13 Aug 2016 03:35:15 -0400 Subject: [PATCH 226/279] Install nose and flake8 if not available for running make tasks (#273) * Install nose and flake8 if not available for running make tasks * Add `py35` to tox.ini envlist * Add task `clean-tox` to Makefile --- .gitignore | 1 + Makefile | 8 +++++++- tox.ini | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 976a959e..3fbef089 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ cover/ venv/ .tox *.egg +.ropeproject/ diff --git a/Makefile b/Makefile index da9fef36..4c204137 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,19 @@ .PHONY: lint test coverage clean clean-pyc clean-build docs lint: + @which flake8 || pip install flake8 @flake8 flask_oauthlib tests test: + @which nosetests || pip install nose @nosetests -s --nologcapture coverage: + @which nosetests || pip install nose @rm -f .coverage @nosetests --with-coverage --cover-package=flask_oauthlib --cover-html -clean: clean-build clean-pyc clean-docs +clean: clean-build clean-pyc clean-docs clean-tox clean-build: @@ -29,5 +32,8 @@ clean-pyc: clean-docs: @rm -fr docs/_build +clean-tox: + @rm -rf .tox/ + docs: @$(MAKE) -C docs html diff --git a/tox.ini b/tox.ini index d0d13b5c..a98a6d00 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py33,py34,pypy +envlist = py26,py27,py33,py34,py35,pypy [testenv] deps = From 391d0f710038669c0d62c3a7287251edd5d47a86 Mon Sep 17 00:00:00 2001 From: "Chang-soo, Han" Date: Wed, 5 Oct 2016 12:20:05 +0900 Subject: [PATCH 227/279] Typo on comment (#291) --- flask_oauthlib/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 71c2d0fd..b76b91f5 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -463,7 +463,7 @@ def request(self, url, data=None, headers=None, format='urlencoded', ) if hasattr(self, 'pre_request'): - # This is desgined for some rubbish service like weibo. + # This is designed for some rubbish services like weibo. # Since they don't follow the standards, we need to # change the uri, headers, or body. uri, headers, body = self.pre_request(uri, headers, body) From 271d6057c29d84da87beb515a4f7e966acf2abc9 Mon Sep 17 00:00:00 2001 From: Luke Lee Date: Tue, 4 Oct 2016 22:22:01 -0500 Subject: [PATCH 228/279] Improve handling of github authorization error (#279) First, thanks for the library! I don't see a way for the `authorized_response` method to ever return `None`. Regardless, it seems possible that the response could be something other than `None` like an `OAuthException` so checking for only `None` seem insufficient. This change fixes the situation where `authorized_response` returns something other than `None` but there is no `access_token` in the response. I've been seeing this fail on my app with a `KeyError` on line 51. So, I'm currently losing the real error from github which could explain *why* the `access_token` is missing. Am I missing something? --- example/github.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/example/github.py b/example/github.py index 63e4af24..039c3aa9 100644 --- a/example/github.py +++ b/example/github.py @@ -42,10 +42,11 @@ def logout(): @app.route('/login/authorized') def authorized(): resp = github.authorized_response() - if resp is None: - return 'Access denied: reason=%s error=%s' % ( + if resp is None or resp.get('access_token') is None: + return 'Access denied: reason=%s error=%s resp=%s' % ( request.args['error'], - request.args['error_description'] + request.args['error_description'], + resp ) session['github_token'] = (resp['access_token'], '') me = github.get('user') From 8a14799a8e7270eab776991c8eae87875f6fab6d Mon Sep 17 00:00:00 2001 From: Vlad Frolov Date: Sun, 13 Nov 2016 04:31:47 +0200 Subject: [PATCH 229/279] Closes #132 and #199: Handle HTTP Basic Auth for client's access to token endpoint (#301) Section 4.1.3 says "If the client type is confidential or the client was issued client credentials (or assigned other authentication requirements), the client MUST authenticate with the authorization server as described in Section 3.2.1.", and that is HTTP Basic Authentication. This commit is based on #292 PR. --- flask_oauthlib/provider/oauth2.py | 68 ++++++++++++++------- flask_oauthlib/utils.py | 5 ++ tests/_base.py | 5 ++ tests/oauth2/test_oauth2.py | 14 ++--- tests/test_oauth2/test_client_credential.py | 13 ++++ tests/test_oauth2/test_code.py | 39 +++++++++--- tests/test_oauth2/test_refresh.py | 16 +++++ 7 files changed, 120 insertions(+), 40 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index ea657d0c..9c6c3c67 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -585,6 +585,33 @@ def __init__(self, clientgetter, tokengetter, grantgetter, self._grantgetter = grantgetter self._grantsetter = grantsetter + def _get_client_creds_from_request(self, request): + """Return client credentials based on the current request. + + According to the rfc6749, client MAY use the HTTP Basic authentication + scheme as defined in [RFC2617] to authenticate with the authorization + server. The client identifier is encoded using the + "application/x-www-form-urlencoded" encoding algorithm per Appendix B, + and the encoded value is used as the username; the client password is + encoded using the same algorithm and used as the password. The + authorization server MUST support the HTTP Basic authentication scheme + for authenticating clients that were issued a client password. + See `Section 2.3.1`_. + + .. _`Section 2.3.1`: https://tools.ietf.org/html/rfc6749#section-2.3.1 + """ + if request.client_id is not None: + return request.client_id, request.client_secret + + auth = request.headers.get('Authorization') + # If Werkzeug successfully parsed the Authorization header, + # `extract_params` helper will replace the header with a parsed dict, + # otherwise, there is nothing useful in the header and we just skip it. + if isinstance(auth, dict): + return auth['username'], auth['password'] + + return None, None + def client_authentication_required(self, request, *args, **kwargs): """Determine if client authentication is required for current request. @@ -599,16 +626,20 @@ def client_authentication_required(self, request, *args, **kwargs): .. _`Section 4.1.3`: http://tools.ietf.org/html/rfc6749#section-4.1.3 .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 """ - def is_confidential(client): + if hasattr(client, 'is_confidential'): + return client.is_confidential client_type = getattr(client, 'client_type', None) - if client_type and client_type == 'confidential': - return True - return getattr(client, 'is_confidential', False) + if client_type: + return client_type == 'confidential' + return True + grant_types = ('password', 'authorization_code', 'refresh_token') - if request.grant_type in grant_types: - client = self._clientgetter(request.client_id) - return (not client) or is_confidential(client) + client_id, _ = self._get_client_creds_from_request(request) + if client_id and request.grant_type in grant_types: + client = self._clientgetter(client_id) + if client: + return is_confidential(client) return False def authenticate_client(self, request, *args, **kwargs): @@ -618,20 +649,8 @@ def authenticate_client(self, request, *args, **kwargs): .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 """ - auth = request.headers.get('Authorization', None) - log.debug('Authenticate client %r', auth) - if auth: - try: - _, s = auth.split(' ') - client_id, client_secret = decode_base64(s).split(':') - client_id = to_unicode(client_id, 'utf-8') - client_secret = to_unicode(client_secret, 'utf-8') - except Exception as e: - log.warning('Authenticate client failed with exception: %r', e, exc_info=True) - return False - else: - client_id = request.client_id - client_secret = request.client_secret + client_id, client_secret = self._get_client_creds_from_request(request) + log.debug('Authenticate client %r', client_id) client = self._clientgetter(client_id) if not client: @@ -643,8 +662,8 @@ def authenticate_client(self, request, *args, **kwargs): # http://tools.ietf.org/html/rfc6749#section-2 # The client MAY omit the parameter if the client secret is an empty string. if hasattr(client, 'client_secret') and client.client_secret != client_secret: - log.debug('Authenticate client failed, secret not match.') - return False + log.debug('Authenticate client failed, secret not match.') + return False log.debug('Authenticate client success.') return True @@ -655,6 +674,9 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): :param client_id: Client ID of the non-confidential client :param request: The Request object passed by oauthlib """ + if client_id is None: + client_id, _ = self._get_client_creds_from_request(request) + log.debug('Authenticate client %r.', client_id) client = request.client or self._clientgetter(client_id) if not client: diff --git a/flask_oauthlib/utils.py b/flask_oauthlib/utils.py index f5921309..5a7b5013 100644 --- a/flask_oauthlib/utils.py +++ b/flask_oauthlib/utils.py @@ -27,6 +27,11 @@ def extract_params(): del headers['wsgi.input'] if 'wsgi.errors' in headers: del headers['wsgi.errors'] + # Werkzeug, and subsequently Flask provide a safe Authorization header + # parsing, so we just replace the Authorization header with the extraced + # info if it was successfully parsed. + if request.authorization: + headers['Authorization'] = request.authorization body = request.form.to_dict() return uri, http_method, body, headers diff --git a/tests/_base.py b/tests/_base.py index 4359d485..dc95d1ff 100644 --- a/tests/_base.py +++ b/tests/_base.py @@ -1,5 +1,6 @@ # coding: utf-8 +import base64 import os import sys import tempfile @@ -91,6 +92,10 @@ def to_bytes(text): return text +def to_base64(text): + return to_unicode(base64.b64encode(to_bytes(text))) + + def clean_url(http://github.com/location): location = to_unicode(location) ret = urlparse(location) diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index 959315fe..d88f4726 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -1,7 +1,6 @@ # coding: utf-8 import json -import base64 from flask import Flask from mock import MagicMock from .server import ( @@ -13,8 +12,7 @@ Token ) from .client import create_client -from .._base import BaseSuite, clean_url -from .._base import to_bytes as b +from .._base import BaseSuite, clean_url, to_base64 from .._base import to_unicode as u @@ -51,11 +49,7 @@ def setup_app(self, app): ) -def _base64(text): - return u(base64.b64encode(b(text))) - - -auth_code = _base64('confidential:confidential') +auth_code = to_base64('confidential:confidential') class TestWebAuth(OAuthSuite): @@ -313,7 +307,7 @@ def test_invalid_auth_header(self): assert b'invalid_client' in rv.data def test_no_client(self): - auth_code = _base64('none:confidential') + auth_code = to_base64('none:confidential') url = ('/oauth/token?grant_type=client_credentials' '&scope=email+address') rv = self.client.get(url, headers={ @@ -322,7 +316,7 @@ def test_no_client(self): assert b'invalid_client' in rv.data def test_wrong_secret_client(self): - auth_code = _base64('confidential:wrong') + auth_code = to_base64('confidential:wrong') url = ('/oauth/token?grant_type=client_credentials' '&scope=email+address') rv = self.client.get(url, headers={ diff --git a/tests/test_oauth2/test_client_credential.py b/tests/test_oauth2/test_client_credential.py index 231dcb71..317a50be 100644 --- a/tests/test_oauth2/test_client_credential.py +++ b/tests/test_oauth2/test_client_credential.py @@ -1,5 +1,6 @@ # coding: utf-8 +from .._base import to_base64 from .base import TestCase from .base import create_server, sqlalchemy_provider, cache_provider from .base import db, Client, User @@ -31,6 +32,18 @@ def test_get_token(self): }) assert b'access_token' in rv.data + rv = self.client.post('/oauth/token', data={ + 'grant_type': 'client_credentials' + }, headers={ + 'authorization': 'Basic ' + to_base64( + '%s:%s' % ( + self.oauth_client.client_id, + self.oauth_client.client_secret + ) + ) + }) + assert b'access_token' in rv.data + class TestSQLAlchemyProvider(TestDefaultProvider): def create_server(self): diff --git a/tests/test_oauth2/test_code.py b/tests/test_oauth2/test_code.py index b6d5a9d2..5df498fa 100644 --- a/tests/test_oauth2/test_code.py +++ b/tests/test_oauth2/test_code.py @@ -1,6 +1,7 @@ # coding: utf-8 from datetime import datetime, timedelta +from .._base import to_base64 from .base import TestCase from .base import create_server, sqlalchemy_provider, cache_provider from .base import db, Client, User, Grant @@ -97,15 +98,39 @@ def test_get_token(self): db.session.add(grant) db.session.commit() - url = ( - '/oauth/token?grant_type=authorization_code' - '&code=test-get-token&client_id=%s' - ) % self.oauth_client.client_id - rv = self.client.get(url) + url = '/oauth/token?grant_type=authorization_code&code=test-get-token' + rv = self.client.get( + url + '&client_id=%s' % (self.oauth_client.client_id) + ) assert b'invalid_client' in rv.data - url += '&client_secret=' + self.oauth_client.client_secret - rv = self.client.get(url) + rv = self.client.get( + url + '&client_id=%s&client_secret=%s' % ( + self.oauth_client.client_id, + self.oauth_client.client_secret + ) + ) + assert b'access_token' in rv.data + + grant = Grant( + user_id=1, + client_id=self.oauth_client.client_id, + scope='email', + redirect_uri='http://localhost/authorized', + code='test-get-token', + expires=expires, + ) + db.session.add(grant) + db.session.commit() + + rv = self.client.get(url, headers={ + 'authorization': 'Basic ' + to_base64( + '%s:%s' % ( + self.oauth_client.client_id, + self.oauth_client.client_secret + ) + ) + }) assert b'access_token' in rv.data diff --git a/tests/test_oauth2/test_refresh.py b/tests/test_oauth2/test_refresh.py index 2a99ea6e..785fcef1 100644 --- a/tests/test_oauth2/test_refresh.py +++ b/tests/test_oauth2/test_refresh.py @@ -1,5 +1,7 @@ # coding: utf-8 +import json +from .._base import to_base64, to_unicode as u from .base import TestCase from .base import create_server, sqlalchemy_provider, cache_provider from .base import db, Client, User, Token @@ -82,6 +84,20 @@ def test_confidential_get_token(self): }) assert b'access_token' in rv.data + token.refresh_token = json.loads(u(rv.data))['refresh_token'] + rv = self.client.post('/oauth/token', data={ + 'grant_type': 'refresh_token', + 'refresh_token': token.refresh_token, + }, headers={ + 'authorization': 'Basic ' + to_base64( + '%s:%s' % ( + self.confidential_client.client_id, + self.confidential_client.client_secret + ) + ) + }) + assert b'access_token' in rv.data + class TestSQLAlchemyProvider(TestDefaultProvider): def create_server(self): From d62bf665fa2a4ab71327cd665334fdec25ade116 Mon Sep 17 00:00:00 2001 From: Quais Taraki Date: Mon, 5 Dec 2016 21:20:23 -0800 Subject: [PATCH 230/279] Issues with remote app config (#304) Line 22 should not have "," for the kwarg and also I believe the authorize_url is for Twitter API version 1 and not 1.1 --- example/twitter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/twitter.py b/example/twitter.py index 0ed61514..02a8d266 100644 --- a/example/twitter.py +++ b/example/twitter.py @@ -19,7 +19,7 @@ base_url='https://api.twitter.com/1.1/', request_token_url='https://api.twitter.com/oauth/request_token', access_token_url='https://api.twitter.com/oauth/access_token', - authorize_url='https://api.twitter.com/oauth/authenticate', + authorize_url='https://api.twitter.com/oauth/authorize' ) From dca541c6d0213b3ec3cc03e0e6057fad129336fa Mon Sep 17 00:00:00 2001 From: Richard Littauer Date: Thu, 16 Mar 2017 22:07:30 -0400 Subject: [PATCH 231/279] Copyedited Oauth2 docs (#302) Just some grammar fixes while I was reading it over. --- docs/oauth2.rst | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/oauth2.rst b/docs/oauth2.rst index 7681d153..0b30630c 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -26,12 +26,12 @@ User (Resource Owner) --------------------- A user, or resource owner, is usually the registered user on your site. You -design your own user model, there is not much to say. +need to design your own user model. Client (Application) --------------------- -A client is the app which want to use the resource of a user. It is suggested +A client is the app which wants to use the resource of a user. It is suggested that the client is registered by a user on your site, but it is not required. The client should contain at least these properties: @@ -107,10 +107,10 @@ Grant Token ----------- A grant token is created in the authorization flow, and will be destroyed -when the authorization finished. In this case, it would be better to store -the data in a cache, which would benefit a better performance. +when the authorization is finished. In this case, it would be better to store +the data in a cache, which leads to better performance. -A grant token should contain at least these information: +A grant token should contain at least this information: - client_id: A random string of client_id - code: A random string @@ -120,7 +120,7 @@ A grant token should contain at least these information: - redirect_uri: A URI string - delete: A function to delete itself -Also in SQLAlchemy model (would be better if it is in a cache):: +Also in an SQLAlchemy model (this should be in a cache):: class Grant(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -159,9 +159,9 @@ Bearer Token A bearer token is the final token that could be used by the client. There are other token types, but bearer token is widely used. Flask-OAuthlib only -comes with bearer token. +comes with a bearer token. -A bearer token requires at least these information: +A bearer token requires at least this information: - access_token: A string token - refresh_token: A string token @@ -209,7 +209,7 @@ An example of the data model in SQLAlchemy:: Configuration ------------- -The oauth provider has some built-in defaults, you can change them with Flask +The Oauth provider has some built-in defaults. You can change them with Flask config: ================================== ========================================== @@ -225,18 +225,18 @@ config: Implementation -------------- -The implementation of authorization flow needs two handlers, one is the authorization +The implementation of the authorization flow needs two handlers: one is the authorization handler for the user to confirm the grant, the other is the token handler for the client to exchange/refresh access tokens. -Before the implementing of authorize and token handler, we need to set up some +Before implementing the authorize and token handlers, we need to set up some getters and setters to communicate with the database. Client getter ````````````` A client getter is required. It tells which client is sending the requests, -creating the getter with decorator:: +creating the getter with a decorator:: @oauth.clientgetter def load_client(client_id): @@ -273,9 +273,9 @@ implemented with decorators:: In the sample code, there is a ``get_current_user`` method, that will return -the current user object, you should implement it yourself. +the current user object. You should implement it yourself. -The ``request`` object is defined by ``OAuthlib``, you can get at least this much +The ``request`` object is defined by ``OAuthlib``. You can get at least this much information: - client: client model object @@ -291,7 +291,7 @@ Token getter and setter ``````````````````````` Token getter and setter are required. They are used in the authorization flow -and accessing resource flow. They are implemented with decorators as follows:: +and the accessing resource flow. They are implemented with decorators as follows:: @oauth.tokengetter def load_token(access_token=None, refresh_token=None): @@ -326,7 +326,7 @@ and accessing resource flow. They are implemented with decorators as follows:: db.session.commit() return tok -The getter will receive two parameters, if you don't need to support refresh +The getter will receive two parameters. If you don't need to support a refresh token, you can just load token by access token. The setter receives ``token`` and ``request`` parameters. The ``token`` is a @@ -375,7 +375,7 @@ that you implemented it this way:: confirm = request.form.get('confirm', 'no') return confirm == 'yes' -The GET request will render a page for user to confirm the grant, parameters in +The GET request will render a page for user to confirm the grant. The parameters in kwargs are: - client_id: id of the client @@ -384,11 +384,11 @@ kwargs are: - redirect_uri: redirect_uri parameter - response_type: response_type parameter -The POST request needs to return a bool value that tells whether user granted +The POST request needs to return a boolean value that tells whether user granted access or not. -There is a ``@require_login`` decorator in the sample code, you should -implement it yourself. +There is a ``@require_login`` decorator in the sample code. You should +implement this yourself. Token handler @@ -430,7 +430,7 @@ The authorization flow is finished, everything should be working now. Revoke handler `````````````` In some cases a user may wish to revoke access given to an application and the -revoke handler makes it possible for an application to programmaticaly revoke +revoke handler makes it possible for an application to programmatically revoke the access given to it. Also here you don't need to do much, allowing POST only is recommended:: @@ -476,7 +476,7 @@ can access the defined resources. .. versionchanged:: 0.5.0 -The ``request`` has an additional property ``oauth``, it contains at least: +The ``request`` has an additional property ``oauth``, which contains at least: - client: client model object - scopes: a list of scopes @@ -490,7 +490,7 @@ The ``request`` has an additional property ``oauth``, it contains at least: Example for OAuth 2 ------------------- -An examplary server (and client) can be found in the tests folder: https://github.com/lepture/flask-oauthlib/tree/master/tests/oauth2 +An example server (and client) can be found in the tests folder: https://github.com/lepture/flask-oauthlib/tree/master/tests/oauth2 Other helpful resources include: - Another example of an OAuth 2 server: https://github.com/lepture/example-oauth2-server From d72f1961cbbf2abbd78107c04b608cfaf3753f7e Mon Sep 17 00:00:00 2001 From: Stian Prestholdt Date: Fri, 17 Mar 2017 03:08:32 +0100 Subject: [PATCH 232/279] Allow having access tokens without expiration date (#311) It should be possible to have valid access tokens that doesn't have an expiration date set. The OAuth RFC recommends having it, but it's not a requirement. We therefore should only validate expiration date on bearer token if it's set. Ref: https://tools.ietf.org/html/rfc6749#section-4.2.2 https://tools.ietf.org/html/rfc6749#section-5.1 --- flask_oauthlib/provider/oauth2.py | 5 +++-- tests/oauth2/server.py | 11 +++++++++-- tests/oauth2/test_oauth2.py | 8 ++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 9c6c3c67..4f721fa7 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -808,7 +808,8 @@ def validate_bearer_token(self, token, scopes, request): return False # validate expires - if datetime.datetime.utcnow() > tok.expires: + if tok.expires is not None and \ + datetime.datetime.utcnow() > tok.expires: msg = 'Bearer token is expired.' request.error_message = msg log.debug(msg) @@ -881,7 +882,7 @@ def validate_grant_type(self, client_id, grant_type, client, request, 'authorization_code', 'password', 'client_credentials', 'refresh_token', ) - + # Grant type is allowed if it is part of the 'allowed_grant_types' # of the selected client or if it is one of the default grant types if hasattr(client, 'allowed_grant_types'): diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index a1d4cb0e..b6945ee3 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -105,8 +105,10 @@ class Token(db.Model): scope = db.Column(db.Text) def __init__(self, **kwargs): - expires_in = kwargs.pop('expires_in') - self.expires = datetime.utcnow() + timedelta(seconds=expires_in) + expires_in = kwargs.pop('expires_in', None) + if expires_in is not None: + self.expires = datetime.utcnow() + timedelta(seconds=expires_in) + for k, v in kwargs.items(): setattr(self, k, v) @@ -232,12 +234,17 @@ def prepare_app(app): user_id=1, client_id='dev', access_token='expired', expires_in=0 ) + access_token2 = Token( + user_id=1, client_id='dev', access_token='never_expire' + ) + try: db.session.add(client1) db.session.add(client2) db.session.add(user) db.session.add(temp_grant) db.session.add(access_token) + db.session.add(access_token2) db.session.commit() except: db.session.rollback() diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index d88f4726..56a6b8fd 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -134,6 +134,14 @@ def get_oauth_token(): rv = self.client.get('/method/put') assert b'token is expired' in rv.data + def test_never_expiring_bear_token(self): + @self.oauth_client.tokengetter + def get_oauth_token(): + return 'never_expire', '' + + rv = self.client.get('/method/put') + assert rv.status_code == 200 + def test_get_client(self): rv = self.client.post(authorize_url, data={'confirm': 'yes'}) rv = self.client.get(clean_url(http://github.com/rv.location)) From 5afbbad55bb22df1909d37c028a9f912931165dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20F=C3=BClling?= Date: Mon, 27 Mar 2017 04:00:08 +0200 Subject: [PATCH 233/279] Fix SQLAlchemy import to avoid warning (#323) --- tests/oauth2/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/oauth2/server.py b/tests/oauth2/server.py index b6945ee3..6a1416b1 100644 --- a/tests/oauth2/server.py +++ b/tests/oauth2/server.py @@ -1,7 +1,7 @@ # coding: utf-8 from datetime import datetime, timedelta from flask import g, render_template, request, jsonify, make_response -from flask.ext.sqlalchemy import SQLAlchemy +from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm import relationship from flask_oauthlib.provider import OAuth2Provider from flask_oauthlib.contrib.oauth2 import bind_sqlalchemy From fabf71db84192a24fe7cb1b4409683bc53c229f6 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 9 Jun 2017 17:04:20 +0900 Subject: [PATCH 234/279] Version bump 0.9.4 --- CHANGES.rst | 10 ++++++++++ flask_oauthlib/__init__.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 472586a5..81c855af 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,16 @@ Changelog Here you can see the full list of changes between each Flask-OAuthlib release. +Version 0.9.4 +------------- + +Released on Jun 9, 2017 + +- Handle HTTP Basic Auth for client's access to token endpoint (#301) +- Allow having access tokens without expiration date (#311) +- Log exception traceback. (#281) + + Version 0.9.3 ------------- diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index 3e28130e..e531f41b 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -7,11 +7,11 @@ remote OAuth enabled applications, and also helps you creating your own OAuth servers. - :copyright: (c) 2013 - 2016 by Hsiaoming Yang. + :copyright: (c) 2013 - 2017 by Hsiaoming Yang. :license: BSD, see LICENSE for more details. """ -__version__ = "0.9.3" +__version__ = "0.9.4" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' From 8a47d6ebdef676fa38baa4846a85200f3a6959bc Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 9 Jun 2017 17:15:26 +0900 Subject: [PATCH 235/279] Update donate link --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 19b330fe..454da005 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Flask-OAuthlib ============== .. image:: https://img.shields.io/badge/donate-lepture-green.svg - :target: https://lepture.herokuapp.com/?amount=1000&reason=lepture%2Fflask-oauthlib + :target: https://typlog.com/donate?amount=10&reason=lepture%2Fflask-oauthlib :alt: Donate lepture .. image:: https://img.shields.io/pypi/wheel/flask-oauthlib.svg :target: https://pypi.python.org/pypi/flask-OAuthlib/ From 13481da30f412c4b83a5715de0f0f0a9dadc915b Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 26 Jun 2017 14:57:20 +0300 Subject: [PATCH 236/279] fix client RSA signature method --- flask_oauthlib/client.py | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index b76b91f5..82be8799 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -227,6 +227,8 @@ def __init__( authorize_url=None, consumer_key=None, consumer_secret=None, + rsa_key=None, + signature_method=None, request_token_params=None, request_token_method=None, access_token_params=None, @@ -245,6 +247,8 @@ def __init__( self._authorize_url = authorize_url self._consumer_key = consumer_key self._consumer_secret = consumer_secret + self._rsa_key = rsa_key + self._signature_method = signature_method self._request_token_params = request_token_params self._request_token_method = request_token_method self._access_token_params = access_token_params @@ -260,10 +264,8 @@ def __init__( # Skip this check if app_key is specified, since the information is # specified in the Flask config, instead. if not app_key: - req_params = self.request_token_params or {} - if req_params.get("signature_method") == oauthlib.oauth1.SIGNATURE_RSA: + if signature_method == oauthlib.oauth1.SIGNATURE_RSA: # check for consumer_key and rsa_key - rsa_key = req_params.get("rsa_key") if not consumer_key or not rsa_key: raise TypeError( "OAuthRemoteApp with RSA authentication requires " @@ -300,6 +302,14 @@ def consumer_key(self): def consumer_secret(self): return self._get_property('consumer_secret') + @cached_property + def rsa_key(self): + return self._get_property('rsa_key') + + @cached_property + def signature_method(self): + return self._get_property('signature_method') + @cached_property def request_token_params(self): return self._get_property('request_token_params', {}) @@ -341,20 +351,29 @@ def _get_property(self, key, default=False): return app.config.get(config_key, default) return app.config[config_key] + def get_oauth1_client_params(self, token): + params = copy(self.request_token_params) or {} + if token and isinstance(token, (tuple, list)): + params["resource_owner_key"] = token[0] + params["resource_owner_secret"] = token[1] + + # Set params for SIGNATURE_RSA + if self.signature_method == oauthlib.oauth1.SIGNATURE_RSA: + params["signature_method"] = self.signature_method + params["rsa_key"] = self.rsa_key + + return params + def make_client(self, token=None): # request_token_url is for oauth1 if self.request_token_url: - params = copy(self.request_token_params) or {} - if token and isinstance(token, (tuple, list)): - params["resource_owner_key"] = token[0] - params["resource_owner_secret"] = token[1] - + # get params for client + params = self.get_oauth1_client_params(token) client = oauthlib.oauth1.Client( client_key=self.consumer_key, client_secret=self.consumer_secret, **params ) - else: if token and isinstance(token, (tuple, list)): token = {'access_token': token[0]} From 012af9a2fbf0a59a5f4420c87d6e0c28e39d34e3 Mon Sep 17 00:00:00 2001 From: kk Date: Fri, 30 Jun 2017 02:55:42 -0500 Subject: [PATCH 237/279] fix #277, bad exception handle (#338) * fix #277, bad exception handle * change log.warning to log.exception --- flask_oauthlib/provider/oauth2.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 4f721fa7..e336ef53 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -396,9 +396,9 @@ def decorated(*args, **kwargs): log.debug('OAuth2Error: %r', e, exc_info=True) return redirect(e.in_uri(redirect_uri)) except Exception as e: - log.warning('Exception caught while processing request, %s.' % e, exc_info=True) + log.exception(e) return redirect(add_params_to_uri( - self.error_uri, {'error': str(e) } + self.error_uri, {'error': str(e)} )) else: @@ -414,12 +414,6 @@ def decorated(*args, **kwargs): except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e, exc_info=True) return redirect(e.in_uri(redirect_uri)) - except Exception as e: - log.warning('Exception caught while processing request, %s.' % e, exc_info=True) - return redirect(add_params_to_uri( - self.error_uri, {'error': str(e) } - )) - if not isinstance(rv, bool): # if is a response or redirect @@ -460,12 +454,11 @@ def confirm_authorization_request(self): log.debug('OAuth2Error: %r', e, exc_info=True) return redirect(e.in_uri(redirect_uri or self.error_uri)) except Exception as e: - log.warning('Exception caught while processing request, %s.' % e, exc_info=True) + log.exception(e) return redirect(add_params_to_uri( - self.error_uri, {'error': str(e) } + self.error_uri, {'error': str(e)} )) - def verify_request(self, scopes): """Verify current request, get the oauth data. From fc0f217cb71bcaee80a2f9645f71fcb293cb19b9 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 11 Aug 2017 14:26:50 +0200 Subject: [PATCH 238/279] Typo correction (#342) --- docs/intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro.rst b/docs/intro.rst index b25d055f..529f4f23 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -17,7 +17,7 @@ promising replacement for `python-oauth2`_. .. _`python-oauth2`: https://pypi.python.org/pypi/oauth2/ There are lots of non-standard services that claim they are oauth providers, but -their APIs are always broken. While rewriteing an oauth extension for Flask, I +their APIs are always broken. While rewriting an oauth extension for Flask, I took them into consideration. Flask-OAuthlib does support these non-standard services. From 0c87c1908afbad191e19de7437edcd01f66f8732 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Mon, 25 Sep 2017 02:59:49 -0700 Subject: [PATCH 239/279] accept oauth response with code in GET args or JSON params, for client-side auth flows --- flask_oauthlib/client.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index b76b91f5..f2673fa7 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -499,7 +499,7 @@ def authorize(self, callback=None, state=None, **kwargs): if params: url += '&' + url_encode(params) else: - assert callback is not None, 'Callback is required OAuth2' + assert callback is not None, 'Callback is required for OAuth2' client = self.make_client() @@ -590,10 +590,10 @@ def get_request_token(self): raise OAuthException('No token available', type='token_missing') return rv - def handle_oauth1_response(self): + def handle_oauth1_response(self, args): """Handles an oauth1 authorization response.""" client = self.make_client() - client.verifier = request.args.get('oauth_verifier') + client.verifier = args.get('oauth_verifier') tup = session.get('%s_oauthtok' % self.name) if not tup: raise OAuthException( @@ -621,15 +621,15 @@ def handle_oauth1_response(self): ) return data - def handle_oauth2_response(self): + def handle_oauth2_response(self, args): """Handles an oauth2 authorization response.""" client = self.make_client() remote_args = { - 'code': request.args.get('code'), + 'code': args.get('code'), 'client_secret': self.consumer_secret, - 'redirect_uri': session.get('%s_oauthredir' % self.name) } + remote_args['redirect_uri'] = session.get('%s_oauthredir' % self.name) log.debug('Prepare oauth2 remote args %r', remote_args) remote_args.update(self.access_token_params) headers = copy(self._access_token_headers) @@ -659,6 +659,7 @@ def handle_oauth2_response(self): data = parse_response(resp, content, content_type=self.content_type) if resp.code not in (200, 201): + log.error(data) raise OAuthException( 'Invalid response from %s' % self.name, type='invalid_response', data=data @@ -671,10 +672,15 @@ def handle_unknown_response(self): def authorized_response(self): """Handles authorization response smartly.""" - if 'oauth_verifier' in request.args: - data = self.handle_oauth1_response() - elif 'code' in request.args: - data = self.handle_oauth2_response() + args = None + if request.is_json: + args = request.get_json() + else: + args = request.args + if 'oauth_verifier' in args: + data = self.handle_oauth1_response(args) + elif 'code' in args: + data = self.handle_oauth2_response(args) else: data = self.handle_unknown_response() From 69b89a8f81209cfc4a4bb047d95956ad601b5b15 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Fri, 29 Sep 2017 06:46:51 -0700 Subject: [PATCH 240/279] allow passing in args from user of the module, not forcing GET args to be used between user-agent and client --- flask_oauthlib/client.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index f2673fa7..13ffa9f7 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -670,12 +670,9 @@ def handle_unknown_response(self): """Handles a unknown authorization response.""" return None - def authorized_response(self): + def authorized_response(self, args=None): """Handles authorization response smartly.""" - args = None - if request.is_json: - args = request.get_json() - else: + if args is None: args = request.args if 'oauth_verifier' in args: data = self.handle_oauth1_response(args) From 223fa265c052a4b211b2b41c0dc008bd53f26069 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Sat, 30 Sep 2017 02:13:52 -0700 Subject: [PATCH 241/279] restoring redirect_uri --- flask_oauthlib/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 13ffa9f7..0b3b6ec3 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -628,8 +628,8 @@ def handle_oauth2_response(self, args): remote_args = { 'code': args.get('code'), 'client_secret': self.consumer_secret, + 'redirect_uri': session.get('%s_oauthredir' % self.name) } - remote_args['redirect_uri'] = session.get('%s_oauthredir' % self.name) log.debug('Prepare oauth2 remote args %r', remote_args) remote_args.update(self.access_token_params) headers = copy(self._access_token_headers) From 964d7379554bc20b79e996dba7ae77ba363c1273 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Sat, 30 Sep 2017 02:14:44 -0700 Subject: [PATCH 242/279] remove error log --- flask_oauthlib/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 0b3b6ec3..ec7e9fe7 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -659,7 +659,6 @@ def handle_oauth2_response(self, args): data = parse_response(resp, content, content_type=self.content_type) if resp.code not in (200, 201): - log.error(data) raise OAuthException( 'Invalid response from %s' % self.name, type='invalid_response', data=data From 257fc70841e05b5a96e3abce095544b35e38f5bd Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 18 Oct 2017 21:24:45 +0900 Subject: [PATCH 243/279] Add supporter --- README.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.rst b/README.rst index 454da005..48e8cb3a 100644 --- a/README.rst +++ b/README.rst @@ -27,6 +27,20 @@ Flask-OAuthlib relies on oauthlib_. .. _oauthlib: https://github.com/idan/oauthlib +Sponsored by +------------ + +If you want to quickly add secure authentication to Flask, feel free to +check out Auth0's PHP API SDK and free plan at `auth0.com/overview`_ +|auth0 image| + +.. _`auth0.com/overview`: https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=flask-oauthlib&utm_content=auth + +.. |auth0 image| image:: https://cdn.auth0.com/styleguide/components/1.0.8/media/logos/img/badge.png + :target: https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=flask-oauthlib&utm_content=auth + :alt: Coverage Status + :width: 18 + Features -------- From 7994fa22e338442873bc008da21310ffe97d8dec Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 18 Oct 2017 21:27:17 +0900 Subject: [PATCH 244/279] Update sponsor's image --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 48e8cb3a..3bba386e 100644 --- a/README.rst +++ b/README.rst @@ -36,10 +36,11 @@ check out Auth0's PHP API SDK and free plan at `auth0.com/overview`_ .. _`auth0.com/overview`: https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=flask-oauthlib&utm_content=auth -.. |auth0 image| image:: https://cdn.auth0.com/styleguide/components/1.0.8/media/logos/img/badge.png +.. |auth0 image| image:: https://user-images.githubusercontent.com/290496/31718461-031a6710-b44b-11e7-80f8-7c5920c73b8f.png :target: https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=flask-oauthlib&utm_content=auth :alt: Coverage Status - :width: 18 + :width: 18px + :height: 18px Features -------- From 359c50f2880bd71aa100540f5ea532c9535e47bd Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 18 Oct 2017 23:12:12 +0900 Subject: [PATCH 245/279] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3bba386e..e9d8945e 100644 --- a/README.rst +++ b/README.rst @@ -31,7 +31,7 @@ Sponsored by ------------ If you want to quickly add secure authentication to Flask, feel free to -check out Auth0's PHP API SDK and free plan at `auth0.com/overview`_ +check out Auth0's Python API SDK and free plan at `auth0.com/overview`_ |auth0 image| .. _`auth0.com/overview`: https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=flask-oauthlib&utm_content=auth From e92c1572452126bc1a8caf60317f18408dbc96ad Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 19 Oct 2017 00:32:57 +0900 Subject: [PATCH 246/279] drop python 2.6 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index a98a6d00..5debd991 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py33,py34,py35,pypy +envlist = py27,py33,py34,py35,pypy [testenv] deps = From 0a9ce898818e3b04a0f80ffaa61283af67e24ea5 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 19 Oct 2017 09:30:01 +0900 Subject: [PATCH 247/279] Add documentation for require_login, close #303 --- docs/oauth2.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/oauth2.rst b/docs/oauth2.rst index 0b30630c..27988c19 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -388,7 +388,9 @@ The POST request needs to return a boolean value that tells whether user granted access or not. There is a ``@require_login`` decorator in the sample code. You should -implement this yourself. +implement this yourself. Here is an `example`_ by Flask documentation. + +.. _`example`: http://flask.pocoo.org/docs/0.12/patterns/viewdecorators/#login-required-decorator Token handler From ed060063a58dddb9d1ddd501e431ed0aa5628dbd Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 26 Oct 2017 22:41:07 -0700 Subject: [PATCH 248/279] Add notification --- README.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.rst b/README.rst index e9d8945e..20f88cad 100644 --- a/README.rst +++ b/README.rst @@ -17,6 +17,14 @@ Flask-OAuthlib :target: https://coveralls.io/r/lepture/flask-oauthlib :alt: Coverage Status +Notification +------------ + + I'm planning to create a whole new authentication library which will contain OAuth 1, OAuth 2, OpenID clients and servers. It would be a ready to use library with Flask and Django frameworks built-in. + + To make sure it will be sustainable, there will be commercial supports and paid services. Support this project now at https://www.patreon.com/lepture + + Subscribe the Newsletter now https://tinyletter.com/authlib. I'll send your news when it's ready. Flask-OAuthlib is an extension to Flask that allows you to interact with remote OAuth enabled applications. On the client site, it is a replacement From f3645dcb9e0bacbef18cba8e8c67554c28105e44 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sat, 4 Nov 2017 20:51:51 +0800 Subject: [PATCH 249/279] Add support for string type token --- flask_oauthlib/client.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index d3dbfde7..3b1cc278 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -27,6 +27,12 @@ log = logging.getLogger('flask_oauthlib') +if PY3: + string_types = (str,) +else: + string_types = (str, unicode) + + __all__ = ('OAuth', 'OAuthRemoteApp', 'OAuthResponse', 'OAuthException') @@ -375,8 +381,11 @@ def make_client(self, token=None): **params ) else: - if token and isinstance(token, (tuple, list)): - token = {'access_token': token[0]} + if token: + if isinstance(token, (tuple, list)): + token = {'access_token': token[0]} + elif isinstance(token, string_types): + token = {'access_token': token} client = oauthlib.oauth2.WebApplicationClient( self.consumer_key, token=token ) From c62bd9d4016c5d09de1355c377a160272419fbde Mon Sep 17 00:00:00 2001 From: Grey Li Date: Thu, 9 Nov 2017 11:12:42 +0800 Subject: [PATCH 250/279] add token type test --- tests/test_client.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 77c4b48b..81d64893 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,6 +3,7 @@ from flask_oauthlib.client import encode_request_data from flask_oauthlib.client import OAuthRemoteApp, OAuth from flask_oauthlib.client import parse_response +from oauthlib.common import PY3 try: import urllib2 as http @@ -184,3 +185,32 @@ class _Faker(object): resp, content = OAuthRemoteApp.http_request('http://example.com/') assert resp.code == 404 assert b'o' in content + + def test_token_types(self): + oauth = OAuth() + remote = oauth.remote_app('remote', + consumer_key='remote key', + consumer_secret='remote secret') + + client_token = {'access_token': 'access token'} + + if not PY3: + unicode_token = u'access token' + client = remote.make_client(token=unicode_token) + assert client.token == client_token + + str_token = 'access token' + client = remote.make_client(token=str_token) + assert client.token == client_token + + list_token = ['access token'] + client = remote.make_client(token=list_token) + assert client.token == client_token + + tuple_token = ('access token',) + client = remote.make_client(token=tuple_token) + assert client.token == client_token + + dict_token = {'access_token': 'access token'} + client = remote.make_client(token=dict_token) + assert client.token == client_token From 5feeb4ae41d4157f8c1f6a114ff2f8309f15b2a3 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sat, 18 Nov 2017 15:46:21 +0900 Subject: [PATCH 251/279] Add Authlib notification on Readme --- README.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 20f88cad..89d98928 100644 --- a/README.rst +++ b/README.rst @@ -20,11 +20,10 @@ Flask-OAuthlib Notification ------------ - I'm planning to create a whole new authentication library which will contain OAuth 1, OAuth 2, OpenID clients and servers. It would be a ready to use library with Flask and Django frameworks built-in. +I'm working on `Authlib Date: Sat, 18 Nov 2017 15:47:06 +0900 Subject: [PATCH 252/279] Fix authlib link --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 89d98928..b70ce5ed 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ Flask-OAuthlib Notification ------------ -I'm working on `Authlib Date: Tue, 13 Feb 2018 21:52:11 -0700 Subject: [PATCH 253/279] Fix typo (#371) configuation -> configuration --- docs/client.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/client.rst b/docs/client.rst index bb459f13..f61ea9f1 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -18,7 +18,7 @@ the imports:: OAuth1 Client ------------- -The difference between OAuth1 and OAuth2 in the configuation is +The difference between OAuth1 and OAuth2 in the configuration is ``request_token_url``. In OAuth1 it is required, in OAuth2 it should be ``None``. From 213cbc018c9ef6ec952381e3777d6505f6c87003 Mon Sep 17 00:00:00 2001 From: Guillaume Grasset Date: Thu, 22 Feb 2018 13:53:22 -0500 Subject: [PATCH 254/279] Add a method to register a custom exception handler. By registering a exception handler, it is now possible to implement a custom response to exception instead of relying on the default redirect. The default redirect leaking error information to the client, it is recommended to implement a exception handler in production environment. --- flask_oauthlib/provider/oauth2.py | 54 +++++++++++++++++++++++++------ tests/test_oauth2/test_code.py | 24 +++++++++++++- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index e336ef53..055c004d 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -72,6 +72,7 @@ def user(): def __init__(self, app=None): self._before_request_funcs = [] self._after_request_funcs = [] + self._exception_handler = None self._invalid_response = None if app: self.init_app(app) @@ -85,6 +86,13 @@ def init_app(self, app): app.extensions = getattr(app, 'extensions', {}) app.extensions['oauthlib.provider.oauth2'] = self + def _on_exception(self, error, redirect_content=None): + + if self._exception_handler: + return self._exception_handler(error, redirect_content) + else: + return redirect(redirect_content) + @cached_property def error_uri(self): """The error page URI. @@ -208,6 +216,34 @@ def valid_after_request(valid, oauth): self._after_request_funcs.append(f) return f + def exception_handler(self, f): + """Register a function as custom exception handler. + + **As the default error handling is leaking error to the client, it is + STRONGLY RECOMMENDED to implement your own handler to mask + the server side errors in production environment.** + + When an error occur during execution, we can + handle the error with with the registered function. The function + accepts two parameters: + - error: the error raised + - redirect_content: the content used in the redirect by default + + usage with the flask error handler :: + @oauth.exception_handler + def custom_exception_handler(error, *args): + raise error + + @app.errorhandler(Exception) + def all_exception_handler(*args): + # any treatment you need for the error + return "Server error", 500 + + If no function is registered, it will do a redirect with ``redirect_content`` as content. + """ + self._exception_handler = f + return f + def invalid_response(self, f): """Register a function for responsing with invalid request. @@ -391,13 +427,13 @@ def decorated(*args, **kwargs): kwargs.update(credentials) except oauth2.FatalClientError as e: log.debug('Fatal client error %r', e, exc_info=True) - return redirect(e.in_uri(self.error_uri)) + return self._on_exception(e, e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e, exc_info=True) - return redirect(e.in_uri(redirect_uri)) + return self._on_exception(e, e.in_uri(redirect_uri)) except Exception as e: log.exception(e) - return redirect(add_params_to_uri( + return self._on_exception(e, add_params_to_uri( self.error_uri, {'error': str(e)} )) @@ -410,10 +446,10 @@ def decorated(*args, **kwargs): rv = f(*args, **kwargs) except oauth2.FatalClientError as e: log.debug('Fatal client error %r', e, exc_info=True) - return redirect(e.in_uri(self.error_uri)) + return self._on_exception(e, e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e, exc_info=True) - return redirect(e.in_uri(redirect_uri)) + return self._on_exception(e, e.in_uri(redirect_uri)) if not isinstance(rv, bool): # if is a response or redirect @@ -422,7 +458,7 @@ def decorated(*args, **kwargs): if not rv: # denied by user e = oauth2.AccessDeniedError() - return redirect(e.in_uri(redirect_uri)) + return self._on_exception(e, e.in_uri(redirect_uri)) return self.confirm_authorization_request() return decorated @@ -449,13 +485,13 @@ def confirm_authorization_request(self): return create_response(*ret) except oauth2.FatalClientError as e: log.debug('Fatal client error %r', e, exc_info=True) - return redirect(e.in_uri(self.error_uri)) + return self._on_exception(e, e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e, exc_info=True) - return redirect(e.in_uri(redirect_uri or self.error_uri)) + return self._on_exception(e, e.in_uri(redirect_uri or self.error_uri)) except Exception as e: log.exception(e) - return redirect(add_params_to_uri( + return self._on_exception(e, add_params_to_uri( self.error_uri, {'error': str(e)} )) diff --git a/tests/test_oauth2/test_code.py b/tests/test_oauth2/test_code.py index 5df498fa..59946a7c 100644 --- a/tests/test_oauth2/test_code.py +++ b/tests/test_oauth2/test_code.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from .._base import to_base64 -from .base import TestCase +from .base import TestCase, default_provider from .base import create_server, sqlalchemy_provider, cache_provider from .base import db, Client, User, Grant @@ -159,3 +159,25 @@ def test_get_token(self): url += '&client_secret=' + self.oauth_client.client_secret rv = self.client.get(url) assert b'access_token' in rv.data + + +class TestProviderWithExceptionHandler(TestCase): + + def prepare_data(self): + oauth = default_provider(self.app) + + @oauth.exception_handler + def custom_exception_handler(error, *args): + raise error + + @self.app.errorhandler(Exception) + def all_exception_handler(*args): + return "Testing server error", 500 + + create_server(self.app, oauth=oauth) + + def test_exception_handler(self): + rv = self.client.get('/oauth/authorize') + + assert rv.status_code == 500 + assert rv.data.decode("utf-8") == "Testing server error" From b13386da57a7cbef9c1529ae37b3980d1a854c48 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Wed, 7 Mar 2018 12:24:45 +0000 Subject: [PATCH 255/279] Add Python 3.6. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5debd991..8c74b31d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py33,py34,py35,pypy +envlist = py27,py33,py34,py35,py36,pypy [testenv] deps = From 862860b2020f3d6bfa0654dfc3ed63bafe0852bc Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Wed, 7 Mar 2018 12:27:45 +0000 Subject: [PATCH 256/279] Add Python 3.6 to Travis. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index b4a67a58..3f0934b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - "2.7" - "3.4" - "3.5" + - "3.6" - "pypy" script: From bd06c64fec62e4f37aaed769d4f5156b30405e26 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Wed, 7 Mar 2018 13:33:59 +0000 Subject: [PATCH 257/279] Update supported oauthlib versions. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ba565170..e0d43b4a 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def fread(filename): license='BSD', install_requires=[ 'Flask', - 'oauthlib>=1.1.2', + 'oauthlib>=1.1.2,!=2.0.3,!=2.0.4,!=2.0.5,<3.0.0', 'requests-oauthlib>=0.6.2', ], tests_require=['nose', 'Flask-SQLAlchemy', 'mock'], From 05ef22f455ac6da37ab056bf44029a333d1f9caa Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Wed, 7 Mar 2018 13:35:14 +0000 Subject: [PATCH 258/279] Update requirements. --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index f6331938..0fb9d95a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -Flask==0.11.1 +Flask==0.12.2 mock==2.0.0 -oauthlib==1.1.2 -requests-oauthlib==0.6.2 +oauthlib==2.0.6 +requests-oauthlib==0.8.0 Flask-SQLAlchemy==2.1 From 62afbf1ac6e3b680969c417f9a52bf202bf44d3d Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Wed, 7 Mar 2018 13:44:02 +0000 Subject: [PATCH 259/279] Suppress SQLAlchemy deprecation warning. --- tests/_base.py | 3 ++- tests/test_oauth2/base.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/_base.py b/tests/_base.py index dc95d1ff..c14bbb3a 100644 --- a/tests/_base.py +++ b/tests/_base.py @@ -32,7 +32,8 @@ def setUp(self): 'OAUTH1_PROVIDER_ENFORCE_SSL': False, 'OAUTH1_PROVIDER_KEY_LENGTH': (3, 30), 'OAUTH1_PROVIDER_REALMS': ['email', 'address'], - 'SQLALCHEMY_DATABASE_URI': 'sqlite:///%s' % self.db_file + 'SQLALCHEMY_DATABASE_URI': 'sqlite:///%s' % self.db_file, + 'SQLALCHEMY_TRACK_MODIFICATIONS': False } app.config.update(config) diff --git a/tests/test_oauth2/base.py b/tests/test_oauth2/base.py index bb8a3f82..2fee2d22 100644 --- a/tests/test_oauth2/base.py +++ b/tests/test_oauth2/base.py @@ -313,6 +313,7 @@ def create_app(self): app.debug = True app.secret_key = 'testing' app.config.update({ - 'SQLALCHEMY_DATABASE_URI': 'sqlite://' + 'SQLALCHEMY_DATABASE_URI': 'sqlite://', + 'SQLALCHEMY_TRACK_MODIFICATIONS': False }) return app From 7a58b6d48a99a2d3aca4e887a4bb56458fcc6e46 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 20 Mar 2018 22:10:25 +0900 Subject: [PATCH 260/279] Add Authlib as replacement for Flask-OAuthlib. --- README.rst | 7 +++---- docs/_templates/sidebarintro.html | 3 +++ docs/index.rst | 4 ++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index b70ce5ed..4fe71a7e 100644 --- a/README.rst +++ b/README.rst @@ -17,11 +17,10 @@ Flask-OAuthlib :target: https://coveralls.io/r/lepture/flask-oauthlib :alt: Coverage Status -Notification ------------- +Notice +------ -I'm working on https://github.com/lepture/authlib which will -be the replacement of this project. +**Please use https://github.com/lepture/authlib instead**. ===== diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 83cdcd09..4c267c54 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -1,3 +1,6 @@ +

Notice

+

Flask-OAuthlib is not maintained well. Please use Authlib instead.

+

Feedback

Feedback is greatly appreciated. If you have any questions, comments, random praise, or anymous threats, shoot me an email.

diff --git a/docs/index.rst b/docs/index.rst index a009d632..75bc88d4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,10 @@ oauthlib_. The client part of Flask-OAuthlib shares the same API as Flask-OAuth, which is pretty and simple. +.. warning:: + + Please use https://github.com/lepture/authlib instead. + Features -------- From 47c7df24be3560487509dab071edb7a0aa63e0c0 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 10 Apr 2018 22:27:56 +0900 Subject: [PATCH 261/279] Add authlib documentation links --- docs/client.rst | 4 ++++ docs/oauth1.rst | 4 ++++ docs/oauth2.rst | 6 +++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/client.rst b/docs/client.rst index f61ea9f1..a9b1ce7e 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -1,6 +1,10 @@ Client ====== +.. note:: + + Please read https://docs.authlib.org/en/latest/client/frameworks.html + The client part keeps the same API as `Flask-OAuth`_. The only changes are the imports:: diff --git a/docs/oauth1.rst b/docs/oauth1.rst index 0b9b56d1..4c5b8a70 100644 --- a/docs/oauth1.rst +++ b/docs/oauth1.rst @@ -1,6 +1,10 @@ OAuth1 Server ============= +.. note:: + + Please read https://docs.authlib.org/en/latest/flask/oauth1.html + This part of documentation covers the tutorial of setting up an OAuth1 provider. An OAuth1 server concerns how to grant the authorization and how to protect the resource. Register an **OAuth** provider:: diff --git a/docs/oauth2.rst b/docs/oauth2.rst index 27988c19..73dd81a7 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -3,6 +3,10 @@ OAuth2 Server ============= +.. note:: + + Please read https://docs.authlib.org/en/latest/flask/oauth2.html + An OAuth2 server concerns how to grant the authorization and how to protect the resource. Register an **OAuth** provider:: @@ -495,5 +499,5 @@ Example for OAuth 2 An example server (and client) can be found in the tests folder: https://github.com/lepture/flask-oauthlib/tree/master/tests/oauth2 Other helpful resources include: - - Another example of an OAuth 2 server: https://github.com/lepture/example-oauth2-server + - Another example of an OAuth 2 server: https://github.com/authlib/example-oauth2-server - An article on how to create an OAuth server: http://lepture.com/en/2013/create-oauth-server. From 50f0924153859e6a76eec120ec3700ee69bde226 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 16 May 2018 10:19:52 +0800 Subject: [PATCH 262/279] Version bump 0.9.5 --- CHANGES.rst | 9 +++++++++ flask_oauthlib/__init__.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 81c855af..aac1ad14 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,15 @@ Changelog Here you can see the full list of changes between each Flask-OAuthlib release. +Version 0.9.5 +------------- + +Released on May 16, 2018 + +- Fix error handlers +- Update supported OAuthlib +- Add support for string type token + Version 0.9.4 ------------- diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index e531f41b..2f58e5e1 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -7,11 +7,11 @@ remote OAuth enabled applications, and also helps you creating your own OAuth servers. - :copyright: (c) 2013 - 2017 by Hsiaoming Yang. + :copyright: (c) 2013 by Hsiaoming Yang. :license: BSD, see LICENSE for more details. """ -__version__ = "0.9.4" +__version__ = "0.9.5" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' From 9082a19dd6615314dd4db7fc71c4f3b2c901bb46 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Fri, 25 May 2018 22:25:53 +0900 Subject: [PATCH 263/279] Change the description for using Authlib. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4fe71a7e..48f2b81d 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ Flask-OAuthlib Notice ------ -**Please use https://github.com/lepture/authlib instead**. +**If you are a company, you should use https://github.com/lepture/authlib instead**. ===== From 85b1da8772956e8725b65f3824d7295319fc92a5 Mon Sep 17 00:00:00 2001 From: Charles Law Date: Tue, 5 Jun 2018 15:58:03 -0700 Subject: [PATCH 264/279] Allow a validate class to be passed into the OAuth2 Provider to be used instead of requiring an instance of a class --- flask_oauthlib/provider/oauth2.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index e336ef53..d182f6ea 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -69,10 +69,11 @@ def user(): return jsonify(request.oauth.user) """ - def __init__(self, app=None): + def __init__(self, app=None, validator_class=None): self._before_request_funcs = [] self._after_request_funcs = [] self._invalid_response = None + self._validator_class = validator_class if app: self.init_app(app) @@ -155,7 +156,10 @@ def validate_client_id(self, client_id): if hasattr(self, '_usergetter'): usergetter = self._usergetter - validator = OAuth2RequestValidator( + validator_class = self._validator_class + if validator_class is None: + validator_class = OAuth2RequestValidator + validator = validator_class( clientgetter=self._clientgetter, tokengetter=self._tokengetter, grantgetter=self._grantgetter, From 7729d6efd47cfacc9caf0dfbaf6942c19b03418d Mon Sep 17 00:00:00 2001 From: "Hong Jen Yee (PCMan)" Date: Thu, 29 Nov 2018 23:53:35 +0800 Subject: [PATCH 265/279] Preserve "state" parameter in the oauth2 handler when non-critical errors happen as required in RFC 6749. * Along with "error", "state" is also passed to redirect_uri if it's present on client request. * Reference: https://tools.ietf.org/html/rfc6749#section-4.1.2 --- flask_oauthlib/provider/oauth2.py | 14 +++++++++++++- tests/oauth2/test_oauth2.py | 13 +++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index d182f6ea..7aa9e96a 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -398,6 +398,10 @@ def decorated(*args, **kwargs): return redirect(e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e, exc_info=True) + # on auth error, we should preserve state if it's present according to RFC 6749 + state = request.values.get('state') + if state and not e.state: + e.state = state # set e.state so e.in_uri() can add the state query parameter to redirect uri return redirect(e.in_uri(redirect_uri)) except Exception as e: log.exception(e) @@ -417,6 +421,10 @@ def decorated(*args, **kwargs): return redirect(e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e, exc_info=True) + # on auth error, we should preserve state if it's present according to RFC 6749 + state = request.values.get('state') + if state and not e.state: + e.state = state # set e.state so e.in_uri() can add the state query parameter to redirect uri return redirect(e.in_uri(redirect_uri)) if not isinstance(rv, bool): @@ -425,7 +433,7 @@ def decorated(*args, **kwargs): if not rv: # denied by user - e = oauth2.AccessDeniedError() + e = oauth2.AccessDeniedError(state=request.values.get('state')) return redirect(e.in_uri(redirect_uri)) return self.confirm_authorization_request() return decorated @@ -456,6 +464,10 @@ def confirm_authorization_request(self): return redirect(e.in_uri(self.error_uri)) except oauth2.OAuth2Error as e: log.debug('OAuth2Error: %r', e, exc_info=True) + # on auth error, we should preserve state if it's present according to RFC 6749 + state = request.values.get('state') + if state and not e.state: + e.state = state # set e.state so e.in_uri() can add the state query parameter to redirect uri return redirect(e.in_uri(redirect_uri or self.error_uri)) except Exception as e: log.exception(e) diff --git a/tests/oauth2/test_oauth2.py b/tests/oauth2/test_oauth2.py index 56a6b8fd..c9c17627 100644 --- a/tests/oauth2/test_oauth2.py +++ b/tests/oauth2/test_oauth2.py @@ -81,12 +81,21 @@ def test_oauth_authorize_valid_url(http://github.com/self): assert 'code=' in rv.location assert 'state' not in rv.location - # test state + # test state on access denied + # According to RFC 6749, state should be preserved on error response if it's present in the client request. + # Reference: https://tools.ietf.org/html/rfc6749#section-4.1.2 + rv = self.client.post(authorize_url + '&state=foo', data=dict( + confirm='no' + )) + assert 'error=access_denied' in rv.location + assert 'state=foo' in rv.location + + # test state on success rv = self.client.post(authorize_url + '&state=foo', data=dict( confirm='yes' )) assert 'code=' in rv.location - assert 'state' in rv.location + assert 'state=foo' in rv.location def test_http_head_oauth_authorize_valid_url(http://github.com/self): rv = self.client.head(authorize_url) From 1379c6b27c17e3aac5ea071f12aca59f02c924b6 Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Tue, 15 Jan 2019 11:36:04 +0100 Subject: [PATCH 266/279] Pin requests-oauthlib version to <1.2.0 1.2.0 requires oauthlib 3, which we are not compatible with --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e0d43b4a..32eeb032 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ def fread(filename): install_requires=[ 'Flask', 'oauthlib>=1.1.2,!=2.0.3,!=2.0.4,!=2.0.5,<3.0.0', - 'requests-oauthlib>=0.6.2', + 'requests-oauthlib>=0.6.2,<1.2.0', ], tests_require=['nose', 'Flask-SQLAlchemy', 'mock'], test_suite='nose.collector', From 2547233e8e75e9449ad06645e5300a599f25e5ec Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 23 May 2019 22:23:12 +0900 Subject: [PATCH 267/279] Update Flask Version to fix vulnerability --- README.rst | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 48f2b81d..9eef2b9c 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Flask-OAuthlib ============== .. image:: https://img.shields.io/badge/donate-lepture-green.svg - :target: https://typlog.com/donate?amount=10&reason=lepture%2Fflask-oauthlib + :target: https://lepture.com/donate :alt: Donate lepture .. image:: https://img.shields.io/pypi/wheel/flask-oauthlib.svg :target: https://pypi.python.org/pypi/flask-OAuthlib/ diff --git a/requirements.txt b/requirements.txt index 0fb9d95a..4cc5d916 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Flask==0.12.2 +Flask>=0.12.3 mock==2.0.0 oauthlib==2.0.6 requests-oauthlib==0.8.0 From 57785c16748f8559a7f7710477ffe11fe257f180 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 10 Oct 2019 22:36:43 +0800 Subject: [PATCH 268/279] Update docs --- docs/client.rst | 2 +- docs/intro.rst | 36 +++++++----------------------------- docs/oauth1.rst | 2 +- docs/oauth2.rst | 2 +- 4 files changed, 10 insertions(+), 32 deletions(-) diff --git a/docs/client.rst b/docs/client.rst index a9b1ce7e..090cd428 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -3,7 +3,7 @@ Client .. note:: - Please read https://docs.authlib.org/en/latest/client/frameworks.html + You SHOULD read `Flask OAuth Client `_ documentation. The client part keeps the same API as `Flask-OAuth`_. The only changes are the imports:: diff --git a/docs/intro.rst b/docs/intro.rst index 529f4f23..af9f4eac 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -4,42 +4,20 @@ Introduction ============ Flask-OAuthlib is designed to be a replacement for Flask-OAuth. It depends on -oauthlib_. +oauthlib. Why --- -The original `Flask-OAuth`_ suffers from lack of maintenance, and oauthlib_ is a -promising replacement for `python-oauth2`_. +No, you should not use this library any more. PLEASE SWITCH to Authlib_ instead. -.. _`Flask-OAuth`: http://pythonhosted.org/Flask-OAuth/ -.. _oauthlib: https://github.com/idan/oauthlib -.. _`python-oauth2`: https://pypi.python.org/pypi/oauth2/ +Find out documentation for: -There are lots of non-standard services that claim they are oauth providers, but -their APIs are always broken. While rewriting an oauth extension for Flask, I -took them into consideration. Flask-OAuthlib does support these non-standard -services. - -Flask-OAuthlib also provides the solution for creating an oauth service. It -supports both oauth1 and oauth2 (with Bearer Token). - -import this ------------ - -Flask-OAuthlib was developed with a few :pep:`20` idioms in mind:: - - >>> import this - - -#. Beautiful is better than ugly. -#. Explicit is better than implicit. -#. Simple is better than complex. -#. Complex is better than complicated. -#. Readability counts. - -All contributions to Flask-OAuthlib should keep these important rules in mind. +1. `Flask OAuth Client `_ +2. `Flask OAuth 1.0 Provider `_ +3. `Flask OAuth 2.0 Provider `_ +.. _Authlib: https://authlib.org/ License ------- diff --git a/docs/oauth1.rst b/docs/oauth1.rst index 4c5b8a70..2485b314 100644 --- a/docs/oauth1.rst +++ b/docs/oauth1.rst @@ -3,7 +3,7 @@ OAuth1 Server .. note:: - Please read https://docs.authlib.org/en/latest/flask/oauth1.html + You SHOULD read `Flask OAuth 1.0 Provider `_ documentation. This part of documentation covers the tutorial of setting up an OAuth1 provider. An OAuth1 server concerns how to grant the authorization and diff --git a/docs/oauth2.rst b/docs/oauth2.rst index 73dd81a7..b0929cb8 100644 --- a/docs/oauth2.rst +++ b/docs/oauth2.rst @@ -5,7 +5,7 @@ OAuth2 Server .. note:: - Please read https://docs.authlib.org/en/latest/flask/oauth2.html + You SHOULD read `Flask OAuth 2.0 Provider `_ documentation. An OAuth2 server concerns how to grant the authorization and how to protect the resource. Register an **OAuth** provider:: From 572922bd8d916970ed66aeec32955c992828a473 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 10 Oct 2019 22:37:59 +0800 Subject: [PATCH 269/279] update readme --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9eef2b9c..0ffeebe7 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ Flask-OAuthlib Notice ------ -**If you are a company, you should use https://github.com/lepture/authlib instead**. +**You SHOULD use https://github.com/lepture/authlib instead**. ===== From 3cf414071c52e6fbceb5a14818132d3077341bc8 Mon Sep 17 00:00:00 2001 From: Tin Nguyen Date: Sun, 16 Aug 2020 13:39:53 +1000 Subject: [PATCH 270/279] fix werkzeug imports error for client.py --- flask_oauthlib/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/client.py b/flask_oauthlib/client.py index 3b1cc278..b4497f46 100644 --- a/flask_oauthlib/client.py +++ b/flask_oauthlib/client.py @@ -15,8 +15,9 @@ from functools import wraps from oauthlib.common import to_unicode, PY3, add_params_to_uri from flask import request, redirect, json, session, current_app -from werkzeug import url_quote, url_decode, url_encode -from werkzeug import parse_options_header, cached_property +from werkzeug.urls import url_quote, url_decode, url_encode +from werkzeug.http import parse_options_header +from werkzeug.utils import cached_property from .utils import to_bytes try: from urlparse import urljoin From 2de09a7098ed7ba3c1a921671a37f2d2241db7f9 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sat, 5 Sep 2020 12:18:23 +0800 Subject: [PATCH 271/279] Fix Werkzeug imports --- flask_oauthlib/provider/oauth1.py | 2 +- flask_oauthlib/provider/oauth2.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/flask_oauthlib/provider/oauth1.py b/flask_oauthlib/provider/oauth1.py index eb5fdba0..ec517e19 100644 --- a/flask_oauthlib/provider/oauth1.py +++ b/flask_oauthlib/provider/oauth1.py @@ -10,7 +10,7 @@ import logging from functools import wraps -from werkzeug import cached_property +from werkzeug.utils import cached_property from flask import request, redirect, url_for from flask import make_response, abort from oauthlib.oauth1 import RequestValidator diff --git a/flask_oauthlib/provider/oauth2.py b/flask_oauthlib/provider/oauth2.py index 23eed415..14a57af2 100644 --- a/flask_oauthlib/provider/oauth2.py +++ b/flask_oauthlib/provider/oauth2.py @@ -14,8 +14,7 @@ from functools import wraps from flask import request, url_for from flask import redirect, abort -from werkzeug import cached_property -from werkzeug.utils import import_string +from werkzeug.utils import import_string, cached_property from oauthlib import oauth2 from oauthlib.oauth2 import RequestValidator, Server from oauthlib.common import to_unicode, add_params_to_uri From 39b452610564ba7eac26c1423d9a7bf5e426557d Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sat, 5 Sep 2020 12:33:40 +0800 Subject: [PATCH 272/279] Change werkzeug.contrib.cache to cachelib --- flask_oauthlib/contrib/cache.py | 4 ++-- requirements.txt | 1 + setup.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/flask_oauthlib/contrib/cache.py b/flask_oauthlib/contrib/cache.py index f8788aa7..6adb62e1 100644 --- a/flask_oauthlib/contrib/cache.py +++ b/flask_oauthlib/contrib/cache.py @@ -1,7 +1,7 @@ # coding: utf-8 -from werkzeug.contrib.cache import NullCache, SimpleCache, FileSystemCache -from werkzeug.contrib.cache import MemcachedCache, RedisCache +from cachelib import NullCache, SimpleCache, FileSystemCache +from cachelib import MemcachedCache, RedisCache class Cache(object): diff --git a/requirements.txt b/requirements.txt index 4cc5d916..47c2c60e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ mock==2.0.0 oauthlib==2.0.6 requests-oauthlib==0.8.0 Flask-SQLAlchemy==2.1 +cachelib==0.1.1 diff --git a/setup.py b/setup.py index 32eeb032..08dff95d 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ def fread(filename): 'Flask', 'oauthlib>=1.1.2,!=2.0.3,!=2.0.4,!=2.0.5,<3.0.0', 'requests-oauthlib>=0.6.2,<1.2.0', + 'cachelib', ], tests_require=['nose', 'Flask-SQLAlchemy', 'mock'], test_suite='nose.collector', From 1baf8cbe21dcfaf6e336a02c5dbc5b06e1ac5c85 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Mon, 7 Sep 2020 10:54:31 +0800 Subject: [PATCH 273/279] Update README, add Links section --- README.rst | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/README.rst b/README.rst index 0ffeebe7..624b1a5e 100644 --- a/README.rst +++ b/README.rst @@ -17,13 +17,12 @@ Flask-OAuthlib :target: https://coveralls.io/r/lepture/flask-oauthlib :alt: Coverage Status + Notice ------ **You SHOULD use https://github.com/lepture/authlib instead**. -===== - Flask-OAuthlib is an extension to Flask that allows you to interact with remote OAuth enabled applications. On the client site, it is a replacement for Flask-OAuth. But it does more than that, it also helps you to create @@ -33,6 +32,7 @@ Flask-OAuthlib relies on oauthlib_. .. _oauthlib: https://github.com/idan/oauthlib + Sponsored by ------------ @@ -48,6 +48,7 @@ check out Auth0's Python API SDK and free plan at `auth0.com/overview`_ :width: 18px :height: 18px + Features -------- @@ -58,10 +59,6 @@ Features - Support OAuth1 provider with HMAC and RSA signature - Support OAuth2 provider with Bearer token -And request more features at `github issues`_. - -.. _`github issues`: https://github.com/lepture/flask-oauthlib/issues - Security Reporting ------------------ @@ -73,25 +70,16 @@ Attachment with patch is welcome. Installation ------------ -Installing flask-oauthlib is simple with pip_:: +Installing flask-oauthlib is simple with pip:: $ pip install Flask-OAuthlib -If you don't have pip installed, try with easy_install:: - - $ easy_install Flask-OAuthlib - -.. _pip: http://www.pip-installer.org/ - - -Additional Notes ----------------- - -We keep documentation at `flask-oauthlib@readthedocs`_. +There is also a `development version `_ on GitHub. -.. _`flask-oauthlib@readthedocs`: https://flask-oauthlib.readthedocs.io -If you are only interested in the client part, you can find some examples -in the ``example`` directory. +Links +----- -There is also a `development version `_ on GitHub. +- Documentation: https://flask-oauthlib.readthedocs.io +- PyPI: https://pypi.org/project/Flask-OAuthlib/ +- Client Examples: https://github.com/lepture/flask-oauthlib/tree/master/example From ed0c57171671185f041eea20c6039576bfec198e Mon Sep 17 00:00:00 2001 From: Grey Li Date: Mon, 7 Sep 2020 11:03:58 +0800 Subject: [PATCH 274/279] Update dev requirements version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 47c2c60e..c0a0427e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ Flask>=0.12.3 mock==2.0.0 oauthlib==2.0.6 requests-oauthlib==0.8.0 -Flask-SQLAlchemy==2.1 +Flask-SQLAlchemy==2.4.4 cachelib==0.1.1 From 68d30a427a26e52c5de55903f63ede989dfaf13b Mon Sep 17 00:00:00 2001 From: Grey Li Date: Mon, 7 Sep 2020 11:04:14 +0800 Subject: [PATCH 275/279] Ready for release 0.9.6 --- CHANGES.rst | 10 ++++++++++ flask_oauthlib/__init__.py | 2 +- setup.py | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index aac1ad14..16efa24d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,15 @@ Changelog Here you can see the full list of changes between each Flask-OAuthlib release. +Version 0.9.6 +------------- + +Released on Sept 7, 2020 + +- Fix dependency conflict with requests-oauthlib +- Fix imports for Werkzeug + + Version 0.9.5 ------------- @@ -12,6 +21,7 @@ Released on May 16, 2018 - Update supported OAuthlib - Add support for string type token + Version 0.9.4 ------------- diff --git a/flask_oauthlib/__init__.py b/flask_oauthlib/__init__.py index 2f58e5e1..a8d492e3 100644 --- a/flask_oauthlib/__init__.py +++ b/flask_oauthlib/__init__.py @@ -11,7 +11,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = "0.9.5" +__version__ = "0.9.6" __author__ = "Hsiaoming Yang " __homepage__ = 'https://github.com/lepture/flask-oauthlib' __license__ = 'BSD' diff --git a/setup.py b/setup.py index 08dff95d..7f36f9eb 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,8 @@ def fread(filename): 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', From 37f2b00a757063db753d69d6db575578be6fa687 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Mon, 7 Sep 2020 11:11:18 +0800 Subject: [PATCH 276/279] Revert "Update dev requirements version" This reverts commit ed0c57171671185f041eea20c6039576bfec198e. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c0a0427e..47c2c60e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ Flask>=0.12.3 mock==2.0.0 oauthlib==2.0.6 requests-oauthlib==0.8.0 -Flask-SQLAlchemy==2.4.4 +Flask-SQLAlchemy==2.1 cachelib==0.1.1 From fead99be976c775403f7ddf9180e1fdff84cd9d9 Mon Sep 17 00:00:00 2001 From: Sam Bellen Date: Thu, 24 Sep 2020 13:27:02 +0200 Subject: [PATCH 277/279] Update Auth0 sponsorship link Hey We recently launched a new page specifically geared towards developers on auth0.com. Can we change the link in the sponsorship message? Thanks again for your open-source work! --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 624b1a5e..f7688983 100644 --- a/README.rst +++ b/README.rst @@ -37,13 +37,13 @@ Sponsored by ------------ If you want to quickly add secure authentication to Flask, feel free to -check out Auth0's Python API SDK and free plan at `auth0.com/overview`_ +check out Auth0's Python API SDK and free plan at `auth0.com/developers`_ |auth0 image| -.. _`auth0.com/overview`: https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=flask-oauthlib&utm_content=auth +.. _`auth0.com/developers`: https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=flask-oauthlib&utm_content=auth .. |auth0 image| image:: https://user-images.githubusercontent.com/290496/31718461-031a6710-b44b-11e7-80f8-7c5920c73b8f.png - :target: https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=flask-oauthlib&utm_content=auth + :target: https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=flask-oauthlib&utm_content=auth :alt: Coverage Status :width: 18px :height: 18px From b5084a699aedc45345f712236a0139ddabaa940a Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Mon, 28 Dec 2020 19:21:09 +1100 Subject: [PATCH 278/279] docs: fix simple typo, usally -> usually There is a small typo in docs/oauth1.rst. Should read `usually` rather than `usally`. --- docs/oauth1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/oauth1.rst b/docs/oauth1.rst index 2485b314..b792fabb 100644 --- a/docs/oauth1.rst +++ b/docs/oauth1.rst @@ -28,7 +28,7 @@ To implemente the oauthorization flow, we need to understand the data model. User (Resource Owner) --------------------- -A user, or resource owner, is usally the registered user on your site. You +A user, or resource owner, is usually the registered user on your site. You design your own user model, there is not much to say. From e58cea86ecec5947656b3ac3cd2839e2323a9b4b Mon Sep 17 00:00:00 2001 From: Muhammad Haleem Date: Sun, 17 Dec 2023 01:31:13 +0500 Subject: [PATCH 279/279] Fixing issue related Flask 3 and oauthlib new version --- flask_oauthlib/utils.py | 6 +++--- requirements.txt | 11 ++++++----- setup.py | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/flask_oauthlib/utils.py b/flask_oauthlib/utils.py index 5a7b5013..a36404df 100644 --- a/flask_oauthlib/utils.py +++ b/flask_oauthlib/utils.py @@ -2,7 +2,7 @@ import base64 from flask import request, Response -from oauthlib.common import to_unicode, bytes_type +from oauthlib.common import to_unicode def _get_uri_from_request(request): @@ -31,7 +31,7 @@ def extract_params(): # parsing, so we just replace the Authorization header with the extraced # info if it was successfully parsed. if request.authorization: - headers['Authorization'] = request.authorization + headers['Authorization'] = str(request.authorization) body = request.form.to_dict() return uri, http_method, body, headers @@ -41,7 +41,7 @@ def to_bytes(text, encoding='utf-8'): """Make sure text is bytes type.""" if not text: return text - if not isinstance(text, bytes_type): + if not isinstance(text, bytes): text = text.encode(encoding) return text diff --git a/requirements.txt b/requirements.txt index 47c2c60e..b7e68221 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ -Flask>=0.12.3 +Flask==3.0.0 mock==2.0.0 -oauthlib==2.0.6 -requests-oauthlib==0.8.0 -Flask-SQLAlchemy==2.1 -cachelib==0.1.1 +oauthlib==3.2.2 +requests-oauthlib==1.3.1 +Flask-SQLAlchemy==3.1.1 +cachelib==0.10.2 + diff --git a/setup.py b/setup.py index 7f36f9eb..018c131e 100644 --- a/setup.py +++ b/setup.py @@ -43,8 +43,8 @@ def fread(filename): license='BSD', install_requires=[ 'Flask', - 'oauthlib>=1.1.2,!=2.0.3,!=2.0.4,!=2.0.5,<3.0.0', - 'requests-oauthlib>=0.6.2,<1.2.0', + 'oauthlib>=3.0.0,<3.2.2', + 'requests-oauthlib>=1.0,<1.3.1', 'cachelib', ], tests_require=['nose', 'Flask-SQLAlchemy', 'mock'],