├── tests ├── __init__.py ├── oauth1 │ ├── __init__.py │ ├── templates │ │ ├── home.html │ │ ├── confirm.html │ │ └── layout.html │ ├── client.py │ ├── test_oauth1.py │ └── server.py ├── oauth2 │ ├── __init__.py │ ├── templates │ │ ├── home.html │ │ ├── confirm.html │ │ └── layout.html │ ├── client.py │ ├── server.py │ └── test_oauth2.py ├── test_contrib │ ├── __init__.py │ └── test_apps.py ├── test_oauth2 │ ├── __init__.py │ ├── test_client_credential.py │ ├── test_implicit.py │ ├── test_refresh.py │ ├── test_password.py │ ├── test_code.py │ └── base.py ├── test_utils.py ├── _base.py └── test_client.py ├── docs ├── changelog.rst ├── contrib.rst ├── _static │ └── flask-oauthlib.png ├── authors.rst ├── _templates │ ├── brand.html │ └── sidebarintro.html ├── install.rst ├── api.rst ├── intro.rst ├── index.rst ├── additional.rst ├── Makefile └── conf.py ├── example ├── contrib │ └── experiment-client │ │ ├── .gitignore │ │ ├── twitter.py │ │ └── douban.py ├── static │ ├── openid.png │ ├── sign-in.png │ └── style.css ├── templates │ ├── layout.html │ └── index.html ├── dropbox.py ├── github.py ├── douban.py ├── facebook.py ├── google.py ├── linkedin.py ├── weibo.py ├── twitter.py ├── reddit.py └── qq.py ├── .coveragerc ├── MANIFEST.in ├── .gitmodules ├── requirements.txt ├── tox.ini ├── flask_oauthlib ├── contrib │ ├── client │ │ ├── signals.py │ │ ├── exceptions.py │ │ ├── structure.py │ │ ├── descriptor.py │ │ ├── __init__.py │ │ └── application.py │ ├── __init__.py │ ├── cache.py │ ├── apps.py │ └── oauth2.py ├── provider │ └── __init__.py ├── __init__.py └── utils.py ├── .gitignore ├── AUTHORS ├── .travis.yml ├── Makefile ├── LICENSE ├── setup.py ├── CONTRIBUTING.rst ├── README.rst └── CHANGES.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/oauth1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/oauth2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_contrib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_oauth2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /example/contrib/experiment-client/.gitignore: -------------------------------------------------------------------------------- 1 | dev.cfg 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = flask_oauthlib/contrib/client/* 3 | -------------------------------------------------------------------------------- /tests/oauth1/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | -------------------------------------------------------------------------------- /tests/oauth2/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CHANGES.rst 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /docs/contrib.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | .. include:: ../CONTRIBUTING.rst 4 | -------------------------------------------------------------------------------- /example/static/openid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/flask-oauthlib/master/example/static/openid.png -------------------------------------------------------------------------------- /example/static/sign-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/flask-oauthlib/master/example/static/sign-in.png -------------------------------------------------------------------------------- /docs/_static/flask-oauthlib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/flask-oauthlib/master/docs/_static/flask-oauthlib.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/_themes"] 2 | path = docs/_themes 3 | url = git://github.com/lepture/flask-sphinx-themes.git 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | mock==1.3.0 3 | oauthlib==0.7.2 4 | requests-oauthlib==0.5.0 5 | Flask-SQLAlchemy==2.0 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26,py27,py33,py34,pypy 3 | 4 | [testenv] 5 | deps = 6 | nose 7 | -rrequirements.txt 8 | commands = nosetests -s 9 | -------------------------------------------------------------------------------- /flask_oauthlib/contrib/client/signals.py: -------------------------------------------------------------------------------- 1 | from flask.signals import Namespace 2 | 3 | __all__ = ['request_token_fetched'] 4 | 5 | _signals = Namespace() 6 | request_token_fetched = _signals.signal('request-token-fetched') 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.egg-info 4 | *.swp 5 | __pycache__ 6 | build 7 | develop-eggs 8 | dist 9 | eggs 10 | parts 11 | .DS_Store 12 | .installed.cfg 13 | docs/_build 14 | cover/ 15 | venv/ 16 | .tox 17 | *.egg 18 | -------------------------------------------------------------------------------- /flask_oauthlib/contrib/client/exceptions.py: -------------------------------------------------------------------------------- 1 | __all__ = ['OAuthException', 'AccessTokenNotFound'] 2 | 3 | 4 | class OAuthException(Exception): 5 | pass 6 | 7 | 8 | class AccessTokenNotFound(OAuthException): 9 | pass 10 | -------------------------------------------------------------------------------- /flask_oauthlib/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | flask_oauthlib.contrib 4 | ~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Contributions for Flask OAuthlib. 7 | 8 | :copyright: (c) 2013 - 2014 by Hsiaoming Yang. 9 | """ 10 | -------------------------------------------------------------------------------- /tests/oauth1/templates/confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |

Allow?

5 |
6 | 7 | 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /tests/oauth2/templates/confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |

Allow?

5 |
6 | 7 | 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | - Hsiaoming Yang http://github.com/lepture 2 | - Randy Topliffe https://github.com/Taar 3 | - Mackenzie Blake Thompson https://github.com/flippmoke 4 | - Ib Lundgren https://github.com/ib-lundgren 5 | - Jiangge Zhang https://github.com/tonyseek 6 | - Stian Prestholdt https://github.com/stianpr 7 | -------------------------------------------------------------------------------- /tests/oauth1/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Auth Provider 6 | 7 | 8 | {% if g.user %} 9 | {{g.user.username}} 10 | {% endif %} 11 | {% block body %}{% endblock %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/oauth2/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Auth Provider 6 | 7 | 8 | {% if g.user %} 9 | {{g.user.username}} 10 | {% endif %} 11 | {% block body %}{% endblock %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | Flask-OAuthlib is written and maintained by Hsiaoming Yang . 5 | 6 | Contributors 7 | ------------ 8 | 9 | People who send patches and suggestions: 10 | 11 | .. include:: ../AUTHORS 12 | 13 | Find more contributors on Github_. 14 | 15 | .. _Github: https://github.com/lepture/flask-oauthlib/contributors 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "pypy" 8 | 9 | script: 10 | - nosetests -s 11 | 12 | after_success: 13 | - pip install coveralls 14 | - coverage run --source=flask_oauthlib setup.py -q nosetests 15 | - coveralls 16 | 17 | branches: 18 | only: 19 | - master 20 | 21 | notifications: 22 | email: false 23 | -------------------------------------------------------------------------------- /flask_oauthlib/provider/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | flask_oauthlib.provider 4 | ~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Implemnts OAuth1 and OAuth2 providers support for Flask. 7 | 8 | :copyright: (c) 2013 - 2014 by Hsiaoming Yang. 9 | """ 10 | 11 | # flake8: noqa 12 | from .oauth1 import OAuth1Provider, OAuth1RequestValidator 13 | from .oauth2 import OAuth2Provider, OAuth2RequestValidator 14 | -------------------------------------------------------------------------------- /docs/_templates/brand.html: -------------------------------------------------------------------------------- 1 | 2 |

Flask-OAuthlib

3 |

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

4 | 5 | {%- block footerinner %} 6 | 7 | Fork me on GitHub 8 | 9 | {%- endblock %} 10 | -------------------------------------------------------------------------------- /docs/_templates/sidebarintro.html: -------------------------------------------------------------------------------- 1 |

Feedback

2 |

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

3 | 4 |

Useful Links

5 | 10 | -------------------------------------------------------------------------------- /flask_oauthlib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask_oauthlib 4 | ~~~~~~~~~~~~~~ 5 | 6 | Flask-OAuthlib is an extension for Flask that allows you to interact with 7 | remote OAuth enabled applications, and also helps you creating your own 8 | OAuth servers. 9 | 10 | :copyright: (c) 2013 - 2016 by Hsiaoming Yang. 11 | :license: BSD, see LICENSE for more details. 12 | """ 13 | 14 | __version__ = "0.9.3" 15 | __author__ = "Hsiaoming Yang " 16 | __homepage__ = 'https://github.com/lepture/flask-oauthlib' 17 | __license__ = 'BSD' 18 | -------------------------------------------------------------------------------- /flask_oauthlib/contrib/client/structure.py: -------------------------------------------------------------------------------- 1 | import operator 2 | 3 | 4 | __all__ = ['OAuth1Response', 'OAuth2Response'] 5 | 6 | 7 | class OAuth1Response(dict): 8 | token = property(operator.itemgetter('oauth_token')) 9 | token_secret = property(operator.itemgetter('oauth_token_secret')) 10 | 11 | 12 | class OAuth2Response(dict): 13 | access_token = property(operator.itemgetter('access_token')) 14 | refresh_token = property(operator.itemgetter('refresh_token')) 15 | token_type = property(operator.itemgetter('token_type')) 16 | expires_in = property(operator.itemgetter('expires_in')) 17 | expires_at = property(operator.itemgetter('expires_at')) 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint test coverage clean clean-pyc clean-build docs 2 | 3 | lint: 4 | @flake8 flask_oauthlib tests 5 | 6 | test: 7 | @nosetests -s --nologcapture 8 | 9 | coverage: 10 | @rm -f .coverage 11 | @nosetests --with-coverage --cover-package=flask_oauthlib --cover-html 12 | 13 | clean: clean-build clean-pyc clean-docs 14 | 15 | 16 | clean-build: 17 | @rm -fr build/ 18 | @rm -fr dist/ 19 | @rm -fr *.egg 20 | @rm -fr *.egg-info 21 | 22 | 23 | clean-pyc: 24 | @find . -name '*.pyc' -exec rm -f {} + 25 | @find . -name '*.pyo' -exec rm -f {} + 26 | @find . -name '*~' -exec rm -f {} + 27 | @find . -name '__pycache__' -exec rm -fr {} + 28 | 29 | clean-docs: 30 | @rm -fr docs/_build 31 | 32 | docs: 33 | @$(MAKE) -C docs html 34 | -------------------------------------------------------------------------------- /example/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block title %}Welcome{% endblock %} | Flask OAuth Example 4 | 6 | 7 | 8 |

Flask OAuth Example

9 | 17 | {% for message in get_flashed_messages() %} 18 |

{{ message }}

19 | {% endfor %} 20 | {% block body %}{% endblock %} 21 | 22 | 23 | -------------------------------------------------------------------------------- /example/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Georgia', serif; 3 | font-size: 16px; 4 | margin: 30px; 5 | padding: 0; 6 | } 7 | 8 | img { 9 | border: none; 10 | } 11 | 12 | a { 13 | color: #335E79; 14 | } 15 | 16 | p.message { 17 | color: #335E79; 18 | padding: 10px; 19 | background: #CADEEB; 20 | } 21 | 22 | p.error { 23 | color: #783232; 24 | padding: 10px; 25 | background: #EBCACA; 26 | } 27 | 28 | input { 29 | font-family: 'Georgia', serif; 30 | font-size: 16px; 31 | border: 1px solid black; 32 | color: #335E79; 33 | padding: 2px; 34 | } 35 | 36 | input[type="submit"] { 37 | background: #CADEEB; 38 | color: #335E79; 39 | border-color: #335E79; 40 | } 41 | 42 | input[name="openid"] { 43 | background: url(openid.png) 4px no-repeat; 44 | padding-left: 24px; 45 | } 46 | 47 | h1, h2 { 48 | font-weight: normal; 49 | } 50 | -------------------------------------------------------------------------------- /example/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

Overview

4 | {% if g.user %} 5 |

6 | Hello {{ g.user.screen_name }}! Wanna tweet something? 7 |

8 |
9 |

10 | 11 | 12 |

13 |
14 | {% if tweets %} 15 |

Your Timeline

16 | 22 | {% endif %} 23 | {% else %} 24 |

25 | Sign in to view your public timeline and to tweet from this 26 | example application. 27 |

28 |

29 | sign in 31 |

32 | {% endif %} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /tests/test_oauth2/test_client_credential.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from .base import TestCase 4 | from .base import create_server, sqlalchemy_provider, cache_provider 5 | from .base import db, Client, User 6 | 7 | 8 | class TestDefaultProvider(TestCase): 9 | def create_server(self): 10 | create_server(self.app) 11 | 12 | def prepare_data(self): 13 | self.create_server() 14 | 15 | oauth_client = Client( 16 | name='ios', client_id='client', client_secret='secret', 17 | _redirect_uris='http://localhost/authorized', 18 | ) 19 | 20 | db.session.add(User(username='foo')) 21 | db.session.add(oauth_client) 22 | db.session.commit() 23 | 24 | self.oauth_client = oauth_client 25 | 26 | def test_get_token(self): 27 | rv = self.client.post('/oauth/token', data={ 28 | 'grant_type': 'client_credentials', 29 | 'client_id': self.oauth_client.client_id, 30 | 'client_secret': self.oauth_client.client_secret, 31 | }) 32 | assert b'access_token' in rv.data 33 | 34 | 35 | class TestSQLAlchemyProvider(TestDefaultProvider): 36 | def create_server(self): 37 | create_server(self.app, sqlalchemy_provider(self.app)) 38 | 39 | 40 | class TestCacheProvider(TestDefaultProvider): 41 | def create_server(self): 42 | create_server(self.app, cache_provider(self.app)) 43 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Installation 4 | ============ 5 | 6 | This part of the documentation covers the installation of Flask-OAuthlib. 7 | 8 | 9 | Pip 10 | --- 11 | 12 | Installing Flask-OAuthlib is simple with `pip `_:: 13 | 14 | $ pip install Flask-OAuthlib 15 | 16 | 17 | Cheeseshop Mirror 18 | ----------------- 19 | 20 | If the Cheeseshop is down, you can also install Flask-OAuthlib from one of the 21 | mirrors. `Crate.io `_ is one of them:: 22 | 23 | $ pip install -i http://simple.crate.io/ Flask-OAuthlib 24 | 25 | 26 | Get the Code 27 | ------------ 28 | 29 | Flask-OAuthlib is actively developed on GitHub, where the code is 30 | `always available `_. 31 | 32 | You can either clone the public repository:: 33 | 34 | git clone git://github.com/lepture/flask-oauthlib.git 35 | 36 | Download the `tarball `_:: 37 | 38 | $ curl -OL https://github.com/lepture/flask-oauthlib/tarball/master 39 | 40 | Or, download the `zipball `_:: 41 | 42 | $ curl -OL https://github.com/lepture/flask-oauthlib/zipball/master 43 | 44 | 45 | Once you have a copy of the source, you can embed it in your Python package, 46 | or install it into your site-packages easily:: 47 | 48 | $ python setup.py install 49 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | Developer Interface 4 | =================== 5 | 6 | This part of the documentation covers the interface of Flask-OAuthlib. 7 | 8 | 9 | Client Reference 10 | ---------------- 11 | 12 | .. module:: flask_oauthlib.client 13 | 14 | .. autoclass:: OAuth 15 | :members: 16 | 17 | .. autoclass:: OAuthRemoteApp 18 | :members: 19 | 20 | .. autoclass:: OAuthResponse 21 | :members: 22 | 23 | .. autoclass:: OAuthException 24 | :members: 25 | 26 | 27 | OAuth1 Provider 28 | --------------- 29 | 30 | .. module:: flask_oauthlib.provider 31 | 32 | .. autoclass:: OAuth1Provider 33 | :members: 34 | 35 | .. autoclass:: OAuth1RequestValidator 36 | :members: 37 | 38 | 39 | OAuth2 Provider 40 | --------------- 41 | 42 | .. autoclass:: OAuth2Provider 43 | :members: 44 | 45 | .. autoclass:: OAuth2RequestValidator 46 | :members: 47 | 48 | 49 | Contrib Reference 50 | ----------------- 51 | 52 | Here are APIs provided by contributors. 53 | 54 | .. module:: flask_oauthlib.contrib.oauth2 55 | 56 | .. autofunction:: bind_sqlalchemy 57 | 58 | .. autofunction:: bind_cache_grant 59 | 60 | 61 | .. automodule:: flask_oauthlib.contrib.apps 62 | 63 | .. autofunction:: douban 64 | .. autofunction:: dropbox 65 | .. autofunction:: facebook 66 | .. autofunction:: github 67 | .. autofunction:: google 68 | .. autofunction:: linkedin 69 | .. autofunction:: twitter 70 | .. autofunction:: weibo 71 | -------------------------------------------------------------------------------- /tests/test_oauth2/test_implicit.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from .base import TestCase 4 | from .base import create_server, sqlalchemy_provider, cache_provider 5 | from .base import db, Client, User 6 | 7 | 8 | class TestDefaultProvider(TestCase): 9 | def create_server(self): 10 | create_server(self.app) 11 | 12 | def prepare_data(self): 13 | self.create_server() 14 | 15 | oauth_client = Client( 16 | name='ios', client_id='imp-client', client_secret='imp-secret', 17 | _redirect_uris='http://localhost/authorized', 18 | ) 19 | 20 | db.session.add(User(username='foo')) 21 | db.session.add(oauth_client) 22 | db.session.commit() 23 | 24 | self.oauth_client = oauth_client 25 | 26 | def test_implicit(self): 27 | rv = self.client.post('/oauth/authorize', data={ 28 | 'response_type': 'token', 29 | 'confirm': 'yes', 30 | 'scope': 'email', 31 | 'client_id': self.oauth_client.client_id, 32 | 'client_secret': self.oauth_client.client_secret, 33 | }) 34 | assert 'access_token' in rv.location 35 | 36 | 37 | class TestSQLAlchemyProvider(TestDefaultProvider): 38 | def create_server(self): 39 | create_server(self.app, sqlalchemy_provider(self.app)) 40 | 41 | 42 | class TestCacheProvider(TestDefaultProvider): 43 | def create_server(self): 44 | create_server(self.app, cache_provider(self.app)) 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 - 2014, Hsiaoming Yang. 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * Neither the name of flask-oauthlib nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /docs/intro.rst: -------------------------------------------------------------------------------- 1 | .. _introduction: 2 | 3 | Introduction 4 | ============ 5 | 6 | Flask-OAuthlib is designed to be a replacement for Flask-OAuth. It depends on 7 | oauthlib_. 8 | 9 | Why 10 | --- 11 | 12 | The original `Flask-OAuth`_ suffers from lack of maintenance, and oauthlib_ is a 13 | promising replacement for `python-oauth2`_. 14 | 15 | .. _`Flask-OAuth`: http://pythonhosted.org/Flask-OAuth/ 16 | .. _oauthlib: https://github.com/idan/oauthlib 17 | .. _`python-oauth2`: https://pypi.python.org/pypi/oauth2/ 18 | 19 | There are lots of non-standard services that claim they are oauth providers, but 20 | their APIs are always broken. While rewriteing an oauth extension for Flask, I 21 | took them into consideration. Flask-OAuthlib does support these non-standard 22 | services. 23 | 24 | Flask-OAuthlib also provides the solution for creating an oauth service. It 25 | supports both oauth1 and oauth2 (with Bearer Token). 26 | 27 | import this 28 | ----------- 29 | 30 | Flask-OAuthlib was developed with a few :pep:`20` idioms in mind:: 31 | 32 | >>> import this 33 | 34 | 35 | #. Beautiful is better than ugly. 36 | #. Explicit is better than implicit. 37 | #. Simple is better than complex. 38 | #. Complex is better than complicated. 39 | #. Readability counts. 40 | 41 | All contributions to Flask-OAuthlib should keep these important rules in mind. 42 | 43 | 44 | License 45 | ------- 46 | 47 | A large number of open source projects in Python are `BSD Licensed`_, and 48 | Flask-OAuthlib is released under `BSD License`_ too. 49 | 50 | .. _`BSD License`: http://opensource.org/licenses/BSD-3-Clause 51 | .. _`BSD Licensed`: http://opensource.org/licenses/BSD-3-Clause 52 | 53 | .. include:: ../LICENSE 54 | -------------------------------------------------------------------------------- /flask_oauthlib/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import base64 4 | from flask import request, Response 5 | from oauthlib.common import to_unicode, bytes_type 6 | 7 | 8 | def _get_uri_from_request(request): 9 | """ 10 | The uri returned from request.uri is not properly urlencoded 11 | (sometimes it's partially urldecoded) This is a weird hack to get 12 | werkzeug to return the proper urlencoded string uri 13 | """ 14 | uri = request.base_url 15 | if request.query_string: 16 | uri += '?' + request.query_string.decode('utf-8') 17 | return uri 18 | 19 | 20 | def extract_params(): 21 | """Extract request params.""" 22 | 23 | uri = _get_uri_from_request(request) 24 | http_method = request.method 25 | headers = dict(request.headers) 26 | if 'wsgi.input' in headers: 27 | del headers['wsgi.input'] 28 | if 'wsgi.errors' in headers: 29 | del headers['wsgi.errors'] 30 | 31 | body = request.form.to_dict() 32 | return uri, http_method, body, headers 33 | 34 | 35 | def to_bytes(text, encoding='utf-8'): 36 | """Make sure text is bytes type.""" 37 | if not text: 38 | return text 39 | if not isinstance(text, bytes_type): 40 | text = text.encode(encoding) 41 | return text 42 | 43 | 44 | def decode_base64(text, encoding='utf-8'): 45 | """Decode base64 string.""" 46 | text = to_bytes(text, encoding) 47 | return to_unicode(base64.b64decode(text), encoding) 48 | 49 | 50 | def create_response(headers, body, status): 51 | """Create response class for Flask.""" 52 | response = Response(body or '') 53 | for k, v in headers.items(): 54 | response.headers[str(k)] = v 55 | 56 | response.status_code = status 57 | return response 58 | -------------------------------------------------------------------------------- /example/dropbox.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session, request, jsonify 2 | from flask_oauthlib.client import OAuth 3 | 4 | 5 | app = Flask(__name__) 6 | app.debug = True 7 | app.secret_key = 'development' 8 | oauth = OAuth(app) 9 | 10 | dropbox = oauth.remote_app( 11 | 'dropbox', 12 | consumer_key='a68mwd4ngywz78d', 13 | consumer_secret='uzz3hr6spb7cspa', 14 | request_token_params={}, 15 | base_url='https://www.dropbox.com/1/', 16 | request_token_url=None, 17 | access_token_method='POST', 18 | access_token_url='https://api.dropbox.com/1/oauth2/token', 19 | authorize_url='https://www.dropbox.com/1/oauth2/authorize', 20 | ) 21 | 22 | 23 | @app.route('/') 24 | def index(): 25 | if 'dropbox_token' in session: 26 | me = dropbox.get('account/info') 27 | return jsonify(me.data) 28 | return redirect(url_for('login')) 29 | 30 | 31 | @app.route('/login') 32 | def login(): 33 | return dropbox.authorize(callback=url_for('authorized', _external=True)) 34 | 35 | 36 | @app.route('/logout') 37 | def logout(): 38 | session.pop('dropbox_token', None) 39 | return redirect(url_for('index')) 40 | 41 | 42 | @app.route('/login/authorized') 43 | def authorized(): 44 | resp = dropbox.authorized_response() 45 | if resp is None: 46 | return 'Access denied: reason=%s error=%s' % ( 47 | request.args['error'], 48 | request.args['error_description'] 49 | ) 50 | session['dropbox_token'] = (resp['access_token'], '') 51 | me = dropbox.get('account/info') 52 | return jsonify(me.data) 53 | 54 | 55 | @dropbox.tokengetter 56 | def get_dropbox_oauth_token(): 57 | return session.get('dropbox_token') 58 | 59 | 60 | if __name__ == '__main__': 61 | app.run() 62 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import wsgiref.util 3 | from contextlib import contextmanager 4 | import mock 5 | import werkzeug.wrappers 6 | from flask_oauthlib.utils import extract_params 7 | from oauthlib.common import Request 8 | 9 | 10 | @contextmanager 11 | def set_flask_request(wsgi_environ): 12 | """ 13 | Test helper context manager that mocks the flask request global I didn't 14 | need the whole request context just to test the functions in helpers and I 15 | wanted to be able to set the raw WSGI environment 16 | """ 17 | environ = {} 18 | environ.update(wsgi_environ) 19 | wsgiref.util.setup_testing_defaults(environ) 20 | r = werkzeug.wrappers.Request(environ) 21 | 22 | with mock.patch.dict(extract_params.__globals__, {'request': r}): 23 | yield 24 | 25 | 26 | class UtilsTestSuite(unittest.TestCase): 27 | 28 | def test_extract_params(self): 29 | with set_flask_request({'QUERY_STRING': 'test=foo&foo=bar'}): 30 | uri, http_method, body, headers = extract_params() 31 | self.assertEquals(uri, 'http://127.0.0.1/?test=foo&foo=bar') 32 | self.assertEquals(http_method, 'GET') 33 | self.assertEquals(body, {}) 34 | self.assertEquals(headers, {'Host': '127.0.0.1'}) 35 | 36 | def test_extract_params_with_urlencoded_json(self): 37 | wsgi_environ = { 38 | 'QUERY_STRING': 'state=%7B%22t%22%3A%22a%22%2C%22i%22%3A%22l%22%7D' 39 | } 40 | with set_flask_request(wsgi_environ): 41 | uri, http_method, body, headers = extract_params() 42 | # Request constructor will try to urldecode the querystring, make 43 | # sure this doesn't fail. 44 | Request(uri, http_method, body, headers) 45 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Flask-OAuthlib documentation master file, created by 2 | sphinx-quickstart on Fri May 17 21:54:48 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. _oauthlib: https://github.com/idan/oauthlib 7 | 8 | Flask-OAuthlib 9 | ============== 10 | 11 | Flask-OAuthlib is designed to be a replacement for Flask-OAuth. It depends on 12 | oauthlib_. 13 | 14 | The client part of Flask-OAuthlib shares the same API as Flask-OAuth, 15 | which is pretty and simple. 16 | 17 | 18 | Features 19 | -------- 20 | 21 | - Support for OAuth 1.0a, 1.0, 1.1, OAuth2 client 22 | - Friendly API (same as Flask-OAuth) 23 | - Direct integration with Flask 24 | - Basic support for remote method invocation of RESTful APIs 25 | - Support OAuth1 provider with HMAC and RSA signature 26 | - Support OAuth2 provider with Bearer token 27 | 28 | 29 | User's Guide 30 | ------------ 31 | 32 | This part of the documentation, which is mostly prose, begins with some 33 | background information about Flask-OAuthlib, then focuses on step-by-step 34 | instructions for getting the most out of Flask-OAuthlib 35 | 36 | .. toctree:: 37 | :maxdepth: 2 38 | 39 | intro 40 | install 41 | client 42 | oauth1 43 | oauth2 44 | additional 45 | 46 | 47 | API Documentation 48 | ----------------- 49 | 50 | If you are looking for information on a specific function, class or method, 51 | this part of the documentation is for you. 52 | 53 | .. toctree:: 54 | :maxdepth: 2 55 | 56 | api 57 | 58 | 59 | Additional Notes 60 | ---------------- 61 | 62 | Contribution guide, legal information and changelog are here. 63 | 64 | .. toctree:: 65 | :maxdepth: 2 66 | 67 | contrib 68 | changelog 69 | authors 70 | -------------------------------------------------------------------------------- /example/github.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session, request, jsonify 2 | from flask_oauthlib.client import OAuth 3 | 4 | 5 | app = Flask(__name__) 6 | app.debug = True 7 | app.secret_key = 'development' 8 | oauth = OAuth(app) 9 | 10 | github = oauth.remote_app( 11 | 'github', 12 | consumer_key='a11a1bda412d928fb39a', 13 | consumer_secret='92b7cf30bc42c49d589a10372c3f9ff3bb310037', 14 | request_token_params={'scope': 'user:email'}, 15 | base_url='https://api.github.com/', 16 | request_token_url=None, 17 | access_token_method='POST', 18 | access_token_url='https://github.com/login/oauth/access_token', 19 | authorize_url='https://github.com/login/oauth/authorize' 20 | ) 21 | 22 | 23 | @app.route('/') 24 | def index(): 25 | if 'github_token' in session: 26 | me = github.get('user') 27 | return jsonify(me.data) 28 | return redirect(url_for('login')) 29 | 30 | 31 | @app.route('/login') 32 | def login(): 33 | return github.authorize(callback=url_for('authorized', _external=True)) 34 | 35 | 36 | @app.route('/logout') 37 | def logout(): 38 | session.pop('github_token', None) 39 | return redirect(url_for('index')) 40 | 41 | 42 | @app.route('/login/authorized') 43 | def authorized(): 44 | resp = github.authorized_response() 45 | if resp is None: 46 | return 'Access denied: reason=%s error=%s' % ( 47 | request.args['error'], 48 | request.args['error_description'] 49 | ) 50 | session['github_token'] = (resp['access_token'], '') 51 | me = github.get('user') 52 | return jsonify(me.data) 53 | 54 | 55 | @github.tokengetter 56 | def get_github_oauth_token(): 57 | return session.get('github_token') 58 | 59 | 60 | if __name__ == '__main__': 61 | app.run() 62 | -------------------------------------------------------------------------------- /example/contrib/experiment-client/twitter.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, url_for, session, jsonify 2 | from flask.ext.oauthlib.contrib.client import OAuth 3 | 4 | 5 | class DefaultConfig(object): 6 | DEBUG = True 7 | SECRET_KEY = 'your-secret-key' 8 | TWITTER_CONSUMER_KEY = 'your-api-key' 9 | TWITTER_CONSUMER_SECRET = 'your-api-secret' 10 | 11 | app = Flask(__name__) 12 | app.config.from_object(DefaultConfig) 13 | app.config.from_pyfile('dev.cfg', silent=True) 14 | 15 | oauth = OAuth(app) 16 | twitter = oauth.remote_app( 17 | name='twitter', 18 | version='1', 19 | endpoint_url='https://api.twitter.com/1.1/', 20 | request_token_url='https://api.twitter.com/oauth/request_token', 21 | access_token_url='https://api.twitter.com/oauth/access_token', 22 | authorization_url='https://api.twitter.com/oauth/authorize') 23 | 24 | 25 | @app.route('/') 26 | def home(): 27 | if oauth_twitter_token(): 28 | response = twitter.get('statuses/home_timeline.json') 29 | return jsonify(response=response.json()) 30 | return 'Login' % url_for('oauth_twitter') 31 | 32 | 33 | @app.route('/auth/twitter') 34 | def oauth_twitter(): 35 | callback_uri = url_for('oauth_twitter_callback', _external=True) 36 | return twitter.authorize(callback_uri) 37 | 38 | 39 | @app.route('/auth/twitter/callback') 40 | def oauth_twitter_callback(): 41 | response = twitter.authorized_response() 42 | if response: 43 | session['token'] = (response.token, response.token_secret) 44 | return repr(dict(response)) 45 | else: 46 | return 'T_T Denied' % (url_for('oauth_twitter')) 47 | 48 | 49 | @twitter.tokengetter 50 | def oauth_twitter_token(): 51 | return session.get('token') 52 | 53 | 54 | if __name__ == '__main__': 55 | app.run() 56 | -------------------------------------------------------------------------------- /example/douban.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session, request, jsonify 2 | from flask_oauthlib.client import OAuth 3 | 4 | 5 | app = Flask(__name__) 6 | app.debug = True 7 | app.secret_key = 'development' 8 | oauth = OAuth(app) 9 | 10 | douban = oauth.remote_app( 11 | 'douban', 12 | consumer_key='0cfc3c5d9f873b1826f4b518de95b148', 13 | consumer_secret='3e209e4f9ecf6a4a', 14 | base_url='https://api.douban.com/', 15 | request_token_url=None, 16 | request_token_params={'scope': 'douban_basic_common,shuo_basic_r'}, 17 | access_token_url='https://www.douban.com/service/auth2/token', 18 | authorize_url='https://www.douban.com/service/auth2/auth', 19 | access_token_method='POST', 20 | ) 21 | 22 | 23 | @app.route('/') 24 | def index(): 25 | if 'douban_token' in session: 26 | resp = douban.get('shuo/v2/statuses/home_timeline') 27 | return jsonify(status=resp.status, data=resp.data) 28 | return redirect(url_for('login')) 29 | 30 | 31 | @app.route('/login') 32 | def login(): 33 | return douban.authorize(callback=url_for('authorized', _external=True)) 34 | 35 | 36 | @app.route('/logout') 37 | def logout(): 38 | session.pop('douban_token', None) 39 | return redirect(url_for('index')) 40 | 41 | 42 | @app.route('/login/authorized') 43 | def authorized(): 44 | resp = douban.authorized_response() 45 | if resp is None: 46 | return 'Access denied: reason=%s error=%s' % ( 47 | request.args['error_reason'], 48 | request.args['error_description'] 49 | ) 50 | session['douban_token'] = (resp['access_token'], '') 51 | return redirect(url_for('index')) 52 | 53 | 54 | @douban.tokengetter 55 | def get_douban_oauth_token(): 56 | return session.get('douban_token') 57 | 58 | 59 | if __name__ == '__main__': 60 | app.run() 61 | -------------------------------------------------------------------------------- /tests/test_contrib/test_apps.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from flask import Flask 4 | from flask_oauthlib.client import OAuth 5 | from flask_oauthlib.contrib.apps import douban, linkedin 6 | from nose.tools import assert_raises 7 | 8 | 9 | class RemoteAppFactorySuite(unittest.TestCase): 10 | 11 | def setUp(self): 12 | self.app = Flask(__name__) 13 | self.oauth = OAuth(self.app) 14 | 15 | def test_douban(self): 16 | assert 'douban.com' in douban.__doc__ 17 | assert ':param scope:' in douban.__doc__ 18 | 19 | c1 = douban.create(self.oauth) 20 | assert 'api.douban.com/v2' in c1.base_url 21 | assert c1.request_token_params.get('scope') == 'douban_basic_common' 22 | 23 | assert_raises(KeyError, lambda: c1.consumer_key) 24 | assert_raises(KeyError, lambda: c1.consumer_secret) 25 | 26 | self.app.config['DOUBAN_CONSUMER_KEY'] = 'douban key' 27 | self.app.config['DOUBAN_CONSUMER_SECRET'] = 'douban secret' 28 | assert c1.consumer_key == 'douban key' 29 | assert c1.consumer_secret == 'douban secret' 30 | 31 | c2 = douban.register_to(self.oauth, 'doudou', scope=['a', 'b']) 32 | assert c2.request_token_params.get('scope') == 'a,b' 33 | 34 | assert_raises(KeyError, lambda: c2.consumer_key) 35 | self.app.config['DOUDOU_CONSUMER_KEY'] = 'douban2 key' 36 | assert c2.consumer_key == 'douban2 key' 37 | 38 | def test_linkedin(self): 39 | c1 = linkedin.create(self.oauth) 40 | assert c1.name == 'linkedin' 41 | assert c1.request_token_params == { 42 | 'state': 'RandomString', 43 | 'scope': 'r_basicprofile', 44 | } 45 | 46 | c2 = linkedin.register_to(self.oauth, name='l2', scope=['c', 'd']) 47 | assert c2.name == 'l2' 48 | assert c2.request_token_params == { 49 | 'state': 'RandomString', 50 | 'scope': 'c,d', 51 | }, c2.request_token_params 52 | -------------------------------------------------------------------------------- /example/facebook.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session, request 2 | from flask_oauthlib.client import OAuth, OAuthException 3 | 4 | 5 | FACEBOOK_APP_ID = '188477911223606' 6 | FACEBOOK_APP_SECRET = '621413ddea2bcc5b2e83d42fc40495de' 7 | 8 | 9 | app = Flask(__name__) 10 | app.debug = True 11 | app.secret_key = 'development' 12 | oauth = OAuth(app) 13 | 14 | facebook = oauth.remote_app( 15 | 'facebook', 16 | consumer_key=FACEBOOK_APP_ID, 17 | consumer_secret=FACEBOOK_APP_SECRET, 18 | request_token_params={'scope': 'email'}, 19 | base_url='https://graph.facebook.com', 20 | request_token_url=None, 21 | access_token_url='/oauth/access_token', 22 | access_token_method='GET', 23 | authorize_url='https://www.facebook.com/dialog/oauth' 24 | ) 25 | 26 | 27 | @app.route('/') 28 | def index(): 29 | return redirect(url_for('login')) 30 | 31 | 32 | @app.route('/login') 33 | def login(): 34 | callback = url_for( 35 | 'facebook_authorized', 36 | next=request.args.get('next') or request.referrer or None, 37 | _external=True 38 | ) 39 | return facebook.authorize(callback=callback) 40 | 41 | 42 | @app.route('/login/authorized') 43 | def facebook_authorized(): 44 | resp = facebook.authorized_response() 45 | if resp is None: 46 | return 'Access denied: reason=%s error=%s' % ( 47 | request.args['error_reason'], 48 | request.args['error_description'] 49 | ) 50 | if isinstance(resp, OAuthException): 51 | return 'Access denied: %s' % resp.message 52 | 53 | session['oauth_token'] = (resp['access_token'], '') 54 | me = facebook.get('/me') 55 | return 'Logged in as id=%s name=%s redirect=%s' % \ 56 | (me.data['id'], me.data['name'], request.args.get('next')) 57 | 58 | 59 | @facebook.tokengetter 60 | def get_facebook_oauth_token(): 61 | return session.get('oauth_token') 62 | 63 | 64 | if __name__ == '__main__': 65 | app.run() 66 | -------------------------------------------------------------------------------- /example/contrib/experiment-client/douban.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, url_for, session, jsonify 2 | from flask.ext.oauthlib.contrib.client import OAuth 3 | 4 | 5 | class AppConfig(object): 6 | DEBUG = True 7 | SECRET_KEY = 'your-secret-key' 8 | DOUBAN_CLIENT_ID = 'your-api-key' 9 | DOUBAN_CLIENT_SECRET = 'your-api-secret' 10 | DOUBAN_SCOPE = [ 11 | 'douban_basic_common', 12 | 'shuo_basic_r', 13 | ] 14 | 15 | app = Flask(__name__) 16 | app.config.from_object(AppConfig) 17 | app.config.from_pyfile('dev.cfg', silent=True) 18 | 19 | oauth = OAuth(app) 20 | # see also https://github.com/requests/requests-oauthlib/pull/138 21 | douban = oauth.remote_app( 22 | name='douban', 23 | version='2', 24 | endpoint_url='https://api.douban.com/', 25 | access_token_url='https://www.douban.com/service/auth2/token', 26 | refresh_token_url='https://www.douban.com/service/auth2/token', 27 | authorization_url='https://www.douban.com/service/auth2/auth', 28 | compliance_fixes='.douban:douban_compliance_fix') 29 | 30 | 31 | @app.route('/') 32 | def home(): 33 | if obtain_douban_token(): 34 | response = douban.get('v2/user/~me') 35 | return jsonify(response=response.json()) 36 | return 'Login' % url_for('oauth_douban') 37 | 38 | 39 | @app.route('/auth/douban') 40 | def oauth_douban(): 41 | callback_uri = url_for('oauth_douban_callback', _external=True) 42 | return douban.authorize(callback_uri) 43 | 44 | 45 | @app.route('/auth/douban/callback') 46 | def oauth_douban_callback(): 47 | response = douban.authorized_response() 48 | if response: 49 | store_douban_token(response) 50 | return repr(dict(response)) 51 | else: 52 | return 'T_T Denied' % (url_for('oauth_douban')) 53 | 54 | 55 | @douban.tokengetter 56 | def obtain_douban_token(): 57 | return session.get('token') 58 | 59 | 60 | @douban.tokensaver 61 | def store_douban_token(token): 62 | session['token'] = token 63 | 64 | 65 | if __name__ == '__main__': 66 | app.run() 67 | -------------------------------------------------------------------------------- /example/google.py: -------------------------------------------------------------------------------- 1 | """ 2 | google example 3 | ~~~~~~~~~~~~~~ 4 | 5 | This example is contributed by Bruno Rocha 6 | 7 | GitHub: https://github.com/rochacbruno 8 | """ 9 | from flask import Flask, redirect, url_for, session, request, jsonify 10 | from flask_oauthlib.client import OAuth 11 | 12 | 13 | app = Flask(__name__) 14 | app.config['GOOGLE_ID'] = "cloud.google.com/console and get your ID" 15 | app.config['GOOGLE_SECRET'] = "cloud.google.com/console and get the secret" 16 | app.debug = True 17 | app.secret_key = 'development' 18 | oauth = OAuth(app) 19 | 20 | google = oauth.remote_app( 21 | 'google', 22 | consumer_key=app.config.get('GOOGLE_ID'), 23 | consumer_secret=app.config.get('GOOGLE_SECRET'), 24 | request_token_params={ 25 | 'scope': 'email' 26 | }, 27 | base_url='https://www.googleapis.com/oauth2/v1/', 28 | request_token_url=None, 29 | access_token_method='POST', 30 | access_token_url='https://accounts.google.com/o/oauth2/token', 31 | authorize_url='https://accounts.google.com/o/oauth2/auth', 32 | ) 33 | 34 | 35 | @app.route('/') 36 | def index(): 37 | if 'google_token' in session: 38 | me = google.get('userinfo') 39 | return jsonify({"data": me.data}) 40 | return redirect(url_for('login')) 41 | 42 | 43 | @app.route('/login') 44 | def login(): 45 | return google.authorize(callback=url_for('authorized', _external=True)) 46 | 47 | 48 | @app.route('/logout') 49 | def logout(): 50 | session.pop('google_token', None) 51 | return redirect(url_for('index')) 52 | 53 | 54 | @app.route('/login/authorized') 55 | def authorized(): 56 | resp = google.authorized_response() 57 | if resp is None: 58 | return 'Access denied: reason=%s error=%s' % ( 59 | request.args['error_reason'], 60 | request.args['error_description'] 61 | ) 62 | session['google_token'] = (resp['access_token'], '') 63 | me = google.get('userinfo') 64 | return jsonify({"data": me.data}) 65 | 66 | 67 | @google.tokengetter 68 | def get_google_oauth_token(): 69 | return session.get('google_token') 70 | 71 | 72 | if __name__ == '__main__': 73 | app.run() 74 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | try: 6 | import multiprocessing 7 | except ImportError: 8 | pass 9 | 10 | try: 11 | from setuptools import setup 12 | except ImportError: 13 | from distutils.core import setup 14 | 15 | from email.utils import parseaddr 16 | import flask_oauthlib 17 | 18 | author, author_email = parseaddr(flask_oauthlib.__author__) 19 | 20 | 21 | def fread(filename): 22 | with open(filename) as f: 23 | return f.read() 24 | 25 | 26 | setup( 27 | name='Flask-OAuthlib', 28 | version=flask_oauthlib.__version__, 29 | author=author, 30 | author_email=author_email, 31 | url=flask_oauthlib.__homepage__, 32 | packages=[ 33 | "flask_oauthlib", 34 | "flask_oauthlib.provider", 35 | "flask_oauthlib.contrib", 36 | "flask_oauthlib.contrib.client", 37 | ], 38 | description="OAuthlib for Flask", 39 | zip_safe=False, 40 | include_package_data=True, 41 | platforms='any', 42 | long_description=fread('README.rst'), 43 | license='BSD', 44 | install_requires=[ 45 | 'Flask', 46 | 'oauthlib>=0.6.2', 47 | 'requests-oauthlib>=0.5.0', 48 | ], 49 | tests_require=['nose', 'Flask-SQLAlchemy', 'mock'], 50 | test_suite='nose.collector', 51 | classifiers=[ 52 | 'Development Status :: 4 - Beta', 53 | 'Environment :: Web Environment', 54 | 'Intended Audience :: Developers', 55 | 'License :: OSI Approved', 56 | 'License :: OSI Approved :: BSD License', 57 | 'Operating System :: MacOS', 58 | 'Operating System :: POSIX', 59 | 'Operating System :: POSIX :: Linux', 60 | 'Programming Language :: Python', 61 | 'Programming Language :: Python :: 2.6', 62 | 'Programming Language :: Python :: 2.7', 63 | 'Programming Language :: Python :: 3.3', 64 | 'Programming Language :: Python :: 3.4', 65 | 'Programming Language :: Python :: Implementation', 66 | 'Programming Language :: Python :: Implementation :: CPython', 67 | 'Programming Language :: Python :: Implementation :: PyPy', 68 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 69 | 'Topic :: Software Development :: Libraries :: Python Modules', 70 | ] 71 | ) 72 | -------------------------------------------------------------------------------- /example/linkedin.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session, request, jsonify 2 | from flask_oauthlib.client import OAuth 3 | 4 | 5 | app = Flask(__name__) 6 | app.debug = True 7 | app.secret_key = 'development' 8 | oauth = OAuth(app) 9 | 10 | linkedin = oauth.remote_app( 11 | 'linkedin', 12 | consumer_key='k8fhkgkkqzub', 13 | consumer_secret='ZZtLETQOQYNDjMrz', 14 | request_token_params={ 15 | 'scope': 'r_basicprofile', 16 | 'state': 'RandomString', 17 | }, 18 | base_url='https://api.linkedin.com/v1/', 19 | request_token_url=None, 20 | access_token_method='POST', 21 | access_token_url='https://www.linkedin.com/uas/oauth2/accessToken', 22 | authorize_url='https://www.linkedin.com/uas/oauth2/authorization', 23 | ) 24 | 25 | 26 | @app.route('/') 27 | def index(): 28 | if 'linkedin_token' in session: 29 | me = linkedin.get('people/~') 30 | return jsonify(me.data) 31 | return redirect(url_for('login')) 32 | 33 | 34 | @app.route('/login') 35 | def login(): 36 | return linkedin.authorize(callback=url_for('authorized', _external=True)) 37 | 38 | 39 | @app.route('/logout') 40 | def logout(): 41 | session.pop('linkedin_token', None) 42 | return redirect(url_for('index')) 43 | 44 | 45 | @app.route('/login/authorized') 46 | def authorized(): 47 | resp = linkedin.authorized_response() 48 | if resp is None: 49 | return 'Access denied: reason=%s error=%s' % ( 50 | request.args['error_reason'], 51 | request.args['error_description'] 52 | ) 53 | session['linkedin_token'] = (resp['access_token'], '') 54 | me = linkedin.get('people/~') 55 | return jsonify(me.data) 56 | 57 | 58 | @linkedin.tokengetter 59 | def get_linkedin_oauth_token(): 60 | return session.get('linkedin_token') 61 | 62 | 63 | def change_linkedin_query(uri, headers, body): 64 | auth = headers.pop('Authorization') 65 | headers['x-li-format'] = 'json' 66 | if auth: 67 | auth = auth.replace('Bearer', '').strip() 68 | if '?' in uri: 69 | uri += '&oauth2_access_token=' + auth 70 | else: 71 | uri += '?oauth2_access_token=' + auth 72 | return uri, headers, body 73 | 74 | linkedin.pre_request = change_linkedin_query 75 | 76 | 77 | if __name__ == '__main__': 78 | app.run() 79 | -------------------------------------------------------------------------------- /flask_oauthlib/contrib/client/descriptor.py: -------------------------------------------------------------------------------- 1 | from flask import current_app, session 2 | 3 | 4 | __all__ = ['OAuthProperty', 'WebSessionData'] 5 | 6 | 7 | class OAuthProperty(object): 8 | """The property which providing config item to remote applications. 9 | 10 | The application classes must have ``name`` to identity themselves. 11 | """ 12 | 13 | _missing = object() 14 | 15 | def __init__(self, name, default=_missing): 16 | self.name = name 17 | self.default = default 18 | 19 | def __get__(self, instance, owner): 20 | if instance is None: 21 | return self 22 | 23 | # instance resources 24 | instance_namespace = vars(instance) 25 | instance_ident = instance.name 26 | 27 | # gets from instance namespace 28 | if self.name in instance_namespace: 29 | return instance_namespace[self.name] 30 | 31 | # gets from app config (or default value) 32 | config_name = '{0}_{1}'.format(instance_ident, self.name).upper() 33 | if config_name not in current_app.config: 34 | if self.default is not self._missing: 35 | return self.default 36 | exception_message = ( 37 | '{0!r} missing {1} \n\n You need to provide it in arguments' 38 | ' `{0.__class__.__name__}(..., {1}="foobar", ...)` or in ' 39 | 'app.config `{2}`').format(instance, self.name, config_name) 40 | raise RuntimeError(exception_message) 41 | return current_app.config[config_name] 42 | 43 | def __set__(self, instance, value): 44 | # assigns into instance namespace 45 | instance_namespace = vars(instance) 46 | instance_namespace[self.name] = value 47 | 48 | 49 | class WebSessionData(object): 50 | """The property which providing accessing of Flask session.""" 51 | 52 | key_format = '_oauth_{0}_{1}' 53 | 54 | def __init__(self, ident): 55 | self.ident = ident 56 | 57 | def make_key(self, instance): 58 | return self.key_format.format(instance.name, self.ident) 59 | 60 | def __get__(self, instance, owner): 61 | if instance is None: 62 | return self 63 | return session.get(self.make_key(instance)) 64 | 65 | def __set__(self, instance, value): 66 | session[self.make_key(instance)] = value 67 | 68 | def __delete__(self, instance): 69 | session.pop(self.make_key(instance), None) 70 | -------------------------------------------------------------------------------- /example/weibo.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session, request, jsonify 2 | from flask_oauthlib.client import OAuth 3 | 4 | 5 | app = Flask(__name__) 6 | app.debug = True 7 | app.secret_key = 'development' 8 | oauth = OAuth(app) 9 | 10 | weibo = oauth.remote_app( 11 | 'weibo', 12 | consumer_key='909122383', 13 | consumer_secret='2cdc60e5e9e14398c1cbdf309f2ebd3a', 14 | request_token_params={'scope': 'email,statuses_to_me_read'}, 15 | base_url='https://api.weibo.com/2/', 16 | authorize_url='https://api.weibo.com/oauth2/authorize', 17 | request_token_url=None, 18 | access_token_method='POST', 19 | access_token_url='https://api.weibo.com/oauth2/access_token', 20 | # since weibo's response is a shit, we need to force parse the content 21 | content_type='application/json', 22 | ) 23 | 24 | 25 | @app.route('/') 26 | def index(): 27 | if 'oauth_token' in session: 28 | access_token = session['oauth_token'][0] 29 | resp = weibo.get('statuses/home_timeline.json') 30 | return jsonify(resp.data) 31 | return redirect(url_for('login')) 32 | 33 | 34 | @app.route('/login') 35 | def login(): 36 | return weibo.authorize(callback=url_for('authorized', 37 | next=request.args.get('next') or request.referrer or None, 38 | _external=True)) 39 | 40 | 41 | @app.route('/logout') 42 | def logout(): 43 | session.pop('oauth_token', None) 44 | return redirect(url_for('index')) 45 | 46 | 47 | @app.route('/login/authorized') 48 | def authorized(): 49 | resp = weibo.authorized_response() 50 | if resp is None: 51 | return 'Access denied: reason=%s error=%s' % ( 52 | request.args['error_reason'], 53 | request.args['error_description'] 54 | ) 55 | session['oauth_token'] = (resp['access_token'], '') 56 | return redirect(url_for('index')) 57 | 58 | 59 | @weibo.tokengetter 60 | def get_weibo_oauth_token(): 61 | return session.get('oauth_token') 62 | 63 | 64 | def change_weibo_header(uri, headers, body): 65 | """Since weibo is a rubbish server, it does not follow the standard, 66 | we need to change the authorization header for it.""" 67 | auth = headers.get('Authorization') 68 | if auth: 69 | auth = auth.replace('Bearer', 'OAuth2') 70 | headers['Authorization'] = auth 71 | return uri, headers, body 72 | 73 | weibo.pre_request = change_weibo_header 74 | 75 | 76 | if __name__ == '__main__': 77 | app.run() 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============= 3 | 4 | First, please do contribute! There are more than one way to contribute, and I will 5 | appreciate any way you choose. 6 | 7 | * introduce Flask-OAuthlib to your friends, let Flask-OAuthlib to be known 8 | * discuss Flask-OAuthlib , and submit bugs with github issues 9 | * improve documentation for Flask-OAuthlib 10 | * send patch with github pull request 11 | 12 | English and Chinese issues are acceptable, talk in your favorite language. 13 | 14 | Pull request and git commit message **must be in English**, if your commit message 15 | is in other language, it will be rejected. 16 | 17 | 18 | Issues 19 | ------ 20 | 21 | When you submit an issue, please format your content, a readable content helps a lot. 22 | You should have a little knowledge on Markdown_. 23 | 24 | .. _Markdown: http://github.github.com/github-flavored-markdown/ 25 | 26 | Code talks. If you can't make yourself understood, show me the code. Please make your 27 | case as simple as possible. 28 | 29 | 30 | Codebase 31 | -------- 32 | 33 | The codebase of Flask-OAuthlib is highly tested and :pep:`8` compatible, as a way 34 | to guarantee functionality and keep all code written in a good style. 35 | 36 | You should follow the code style. Here are some tips to make things simple: 37 | 38 | * When you cloned this repo, run ``pip install -r requirements.txt`` 39 | * Check the code style with ``make lint`` 40 | * Check the test with ``make test`` 41 | * Check the test coverage with ``make coverage`` 42 | 43 | 44 | Git Help 45 | -------- 46 | 47 | Something you should know about git. 48 | 49 | * don't add any code on the master branch, create a new one 50 | * don't add too many code in one pull request 51 | * all featured branches should be based on the master branch 52 | * don't merge any code yourself 53 | 54 | Take an example, if you want to add feature A and feature B, you should have two 55 | branches:: 56 | 57 | $ git branch feature-A 58 | $ git checkout feature-A 59 | 60 | Now code on feature-A branch, and when you finish feature A:: 61 | 62 | $ git checkout master 63 | $ git branch feature-B 64 | $ git checkout feature-B 65 | 66 | All branches must be based on the master branch. If your feature-B needs feature-A, 67 | you should send feature-A first, and wait for its merging. We may reject feature-A, 68 | and you should stop feature-B. 69 | 70 | Keep your master branch the same with upstream:: 71 | 72 | $ git remote add upstream git@github.com:lepture/flask-oauthlib.git 73 | $ git pull upstream master 74 | 75 | **And don't change any code on master branch.** 76 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-OAuthlib 2 | ============== 3 | 4 | .. image:: https://img.shields.io/badge/donate-lepture-green.svg 5 | :target: https://lepture.herokuapp.com/?amount=1000&reason=lepture%2Fflask-oauthlib 6 | :alt: Donate lepture 7 | .. image:: https://img.shields.io/pypi/wheel/flask-oauthlib.svg 8 | :target: https://pypi.python.org/pypi/flask-OAuthlib/ 9 | :alt: Wheel Status 10 | .. image:: https://img.shields.io/pypi/v/flask-oauthlib.svg 11 | :target: https://pypi.python.org/pypi/flask-oauthlib/ 12 | :alt: Latest Version 13 | .. image:: https://travis-ci.org/lepture/flask-oauthlib.svg?branch=master 14 | :target: https://travis-ci.org/lepture/flask-oauthlib 15 | :alt: Travis CI Status 16 | .. image:: https://coveralls.io/repos/lepture/flask-oauthlib/badge.svg?branch=master 17 | :target: https://coveralls.io/r/lepture/flask-oauthlib 18 | :alt: Coverage Status 19 | 20 | 21 | Flask-OAuthlib is an extension to Flask that allows you to interact with 22 | remote OAuth enabled applications. On the client site, it is a replacement 23 | for Flask-OAuth. But it does more than that, it also helps you to create 24 | OAuth providers. 25 | 26 | Flask-OAuthlib relies on oauthlib_. 27 | 28 | .. _oauthlib: https://github.com/idan/oauthlib 29 | 30 | Features 31 | -------- 32 | 33 | - Support for OAuth 1.0a, 1.0, 1.1, OAuth2 client 34 | - Friendly API (same as Flask-OAuth) 35 | - Direct integration with Flask 36 | - Basic support for remote method invocation of RESTful APIs 37 | - Support OAuth1 provider with HMAC and RSA signature 38 | - Support OAuth2 provider with Bearer token 39 | 40 | And request more features at `github issues`_. 41 | 42 | .. _`github issues`: https://github.com/lepture/flask-oauthlib/issues 43 | 44 | 45 | Security Reporting 46 | ------------------ 47 | 48 | If you found security bugs which can not be public, send me email at `me@lepture.com`. 49 | Attachment with patch is welcome. 50 | 51 | 52 | Installation 53 | ------------ 54 | 55 | Installing flask-oauthlib is simple with pip_:: 56 | 57 | $ pip install Flask-OAuthlib 58 | 59 | If you don't have pip installed, try with easy_install:: 60 | 61 | $ easy_install Flask-OAuthlib 62 | 63 | .. _pip: http://www.pip-installer.org/ 64 | 65 | 66 | Additional Notes 67 | ---------------- 68 | 69 | We keep documentation at `flask-oauthlib@readthedocs`_. 70 | 71 | .. _`flask-oauthlib@readthedocs`: https://flask-oauthlib.readthedocs.io 72 | 73 | If you are only interested in the client part, you can find some examples 74 | in the ``example`` directory. 75 | 76 | There is also a `development version `_ on GitHub. 77 | -------------------------------------------------------------------------------- /tests/oauth1/client.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session, request, jsonify, abort 2 | from flask_oauthlib.client import OAuth 3 | 4 | 5 | def create_oauth(app): 6 | oauth = OAuth(app) 7 | 8 | remote = oauth.remote_app( 9 | 'dev', 10 | consumer_key='dev', 11 | consumer_secret='dev', 12 | request_token_params={'realm': 'email'}, 13 | base_url='http://127.0.0.1:5000/api/', 14 | request_token_url='http://127.0.0.1:5000/oauth/request_token', 15 | access_token_method='GET', 16 | access_token_url='http://127.0.0.1:5000/oauth/access_token', 17 | authorize_url='http://127.0.0.1:5000/oauth/authorize' 18 | ) 19 | return remote 20 | 21 | 22 | def create_client(app, oauth=None): 23 | if not oauth: 24 | oauth = create_oauth(app) 25 | 26 | @app.route('/') 27 | def index(): 28 | if 'dev_oauth' in session: 29 | ret = oauth.get('email') 30 | if isinstance(ret.data, dict): 31 | return jsonify(ret.data) 32 | return str(ret.data) 33 | return redirect(url_for('login')) 34 | 35 | @app.route('/login') 36 | def login(): 37 | return oauth.authorize(callback=url_for('authorized', _external=True)) 38 | 39 | @app.route('/logout') 40 | def logout(): 41 | session.pop('dev_oauth', None) 42 | return redirect(url_for('index')) 43 | 44 | @app.route('/authorized') 45 | def authorized(): 46 | resp = oauth.authorized_response() 47 | if resp is None: 48 | return 'Access denied: error=%s' % ( 49 | request.args['error'] 50 | ) 51 | if 'oauth_token' in resp: 52 | session['dev_oauth'] = resp 53 | return jsonify(resp) 54 | return str(resp) 55 | 56 | @app.route('/address') 57 | def address(): 58 | ret = oauth.get('address/hangzhou') 59 | if ret.status not in (200, 201): 60 | return abort(ret.status) 61 | return ret.raw_data 62 | 63 | @app.route('/method/') 64 | def method(name): 65 | func = getattr(oauth, name) 66 | ret = func('method') 67 | return ret.raw_data 68 | 69 | @oauth.tokengetter 70 | def get_oauth_token(): 71 | if 'dev_oauth' in session: 72 | resp = session['dev_oauth'] 73 | return resp['oauth_token'], resp['oauth_token_secret'] 74 | 75 | return oauth 76 | 77 | 78 | if __name__ == '__main__': 79 | app = Flask(__name__) 80 | app.debug = True 81 | app.secret_key = 'development' 82 | create_client(app) 83 | app.run(host='localhost', port=8000) 84 | -------------------------------------------------------------------------------- /tests/oauth2/client.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session, request, jsonify, abort 2 | from flask_oauthlib.client import OAuth 3 | 4 | 5 | def create_client(app): 6 | oauth = OAuth(app) 7 | 8 | remote = oauth.remote_app( 9 | 'dev', 10 | consumer_key='dev', 11 | consumer_secret='dev', 12 | request_token_params={'scope': 'email'}, 13 | base_url='http://127.0.0.1:5000/api/', 14 | request_token_url=None, 15 | access_token_method='POST', 16 | access_token_url='http://127.0.0.1:5000/oauth/token', 17 | authorize_url='http://127.0.0.1:5000/oauth/authorize' 18 | ) 19 | 20 | @app.route('/') 21 | def index(): 22 | if 'dev_token' in session: 23 | ret = remote.get('email') 24 | return jsonify(ret.data) 25 | return redirect(url_for('login')) 26 | 27 | @app.route('/login') 28 | def login(): 29 | return remote.authorize(callback=url_for('authorized', _external=True)) 30 | 31 | @app.route('/logout') 32 | def logout(): 33 | session.pop('dev_token', None) 34 | return redirect(url_for('index')) 35 | 36 | @app.route('/authorized') 37 | def authorized(): 38 | resp = remote.authorized_response() 39 | if resp is None: 40 | return 'Access denied: error=%s' % ( 41 | request.args['error'] 42 | ) 43 | if isinstance(resp, dict) and 'access_token' in resp: 44 | session['dev_token'] = (resp['access_token'], '') 45 | return jsonify(resp) 46 | return str(resp) 47 | 48 | @app.route('/client') 49 | def client_method(): 50 | ret = remote.get("client") 51 | if ret.status not in (200, 201): 52 | return abort(ret.status) 53 | return ret.raw_data 54 | 55 | @app.route('/address') 56 | def address(): 57 | ret = remote.get('address/hangzhou') 58 | if ret.status not in (200, 201): 59 | return ret.raw_data, ret.status 60 | return ret.raw_data 61 | 62 | @app.route('/method/') 63 | def method(name): 64 | func = getattr(remote, name) 65 | ret = func('method') 66 | return ret.raw_data 67 | 68 | @remote.tokengetter 69 | def get_oauth_token(): 70 | return session.get('dev_token') 71 | 72 | return remote 73 | 74 | 75 | if __name__ == '__main__': 76 | import os 77 | os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = 'true' 78 | # DEBUG=1 python oauth2_client.py 79 | app = Flask(__name__) 80 | app.debug = True 81 | app.secret_key = 'development' 82 | create_client(app) 83 | app.run(host='localhost', port=8000) 84 | -------------------------------------------------------------------------------- /tests/_base.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import sys 5 | import tempfile 6 | import unittest 7 | from flask_oauthlib.client import prepare_request 8 | try: 9 | from urlparse import urlparse 10 | except ImportError: 11 | from urllib.parse import urlparse 12 | 13 | if sys.version_info[0] == 3: 14 | python_version = 3 15 | string_type = str 16 | else: 17 | python_version = 2 18 | string_type = unicode 19 | 20 | # os.environ['DEBUG'] = 'true' 21 | # for oauthlib 0.6.3 22 | os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = 'true' 23 | 24 | 25 | class BaseSuite(unittest.TestCase): 26 | def setUp(self): 27 | app = self.create_app() 28 | 29 | self.db_fd, self.db_file = tempfile.mkstemp() 30 | config = { 31 | 'OAUTH1_PROVIDER_ENFORCE_SSL': False, 32 | 'OAUTH1_PROVIDER_KEY_LENGTH': (3, 30), 33 | 'OAUTH1_PROVIDER_REALMS': ['email', 'address'], 34 | 'SQLALCHEMY_DATABASE_URI': 'sqlite:///%s' % self.db_file 35 | } 36 | app.config.update(config) 37 | 38 | self.setup_app(app) 39 | 40 | self.app = app 41 | self.client = app.test_client() 42 | return app 43 | 44 | def tearDown(self): 45 | self.database.session.remove() 46 | self.database.drop_all() 47 | 48 | os.close(self.db_fd) 49 | os.unlink(self.db_file) 50 | 51 | @property 52 | def database(self): 53 | raise NotImplementedError 54 | 55 | def create_app(self): 56 | raise NotImplementedError 57 | 58 | def setup_app(self, app): 59 | raise NotImplementedError 60 | 61 | def patch_request(self, app): 62 | test_client = app.test_client() 63 | 64 | def make_request(uri, headers=None, data=None, method=None): 65 | uri, headers, data, method = prepare_request( 66 | uri, headers, data, method 67 | ) 68 | 69 | # test client is a `werkzeug.test.Client` 70 | parsed = urlparse(uri) 71 | uri = '%s?%s' % (parsed.path, parsed.query) 72 | resp = test_client.open( 73 | uri, headers=headers, data=data, method=method 74 | ) 75 | # for compatible 76 | resp.code = resp.status_code 77 | return resp, resp.data 78 | 79 | return make_request 80 | 81 | 82 | def to_unicode(text): 83 | if not isinstance(text, string_type): 84 | text = text.decode('utf-8') 85 | return text 86 | 87 | 88 | def to_bytes(text): 89 | if isinstance(text, string_type): 90 | text = text.encode('utf-8') 91 | return text 92 | 93 | 94 | def clean_url(location): 95 | location = to_unicode(location) 96 | ret = urlparse(location) 97 | return '%s?%s' % (ret.path, ret.query) 98 | -------------------------------------------------------------------------------- /example/twitter.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from flask import Flask 4 | from flask import g, session, request, url_for, flash 5 | from flask import redirect, render_template 6 | from flask_oauthlib.client import OAuth 7 | 8 | 9 | app = Flask(__name__) 10 | app.debug = True 11 | app.secret_key = 'development' 12 | 13 | oauth = OAuth(app) 14 | 15 | twitter = oauth.remote_app( 16 | 'twitter', 17 | consumer_key='xBeXxg9lyElUgwZT6AZ0A', 18 | consumer_secret='aawnSpNTOVuDCjx7HMh6uSXetjNN8zWLpZwCEU4LBrk', 19 | base_url='https://api.twitter.com/1.1/', 20 | request_token_url='https://api.twitter.com/oauth/request_token', 21 | access_token_url='https://api.twitter.com/oauth/access_token', 22 | authorize_url='https://api.twitter.com/oauth/authenticate', 23 | ) 24 | 25 | 26 | @twitter.tokengetter 27 | def get_twitter_token(): 28 | if 'twitter_oauth' in session: 29 | resp = session['twitter_oauth'] 30 | return resp['oauth_token'], resp['oauth_token_secret'] 31 | 32 | 33 | @app.before_request 34 | def before_request(): 35 | g.user = None 36 | if 'twitter_oauth' in session: 37 | g.user = session['twitter_oauth'] 38 | 39 | 40 | @app.route('/') 41 | def index(): 42 | tweets = None 43 | if g.user is not None: 44 | resp = twitter.request('statuses/home_timeline.json') 45 | if resp.status == 200: 46 | tweets = resp.data 47 | else: 48 | flash('Unable to load tweets from Twitter.') 49 | return render_template('index.html', tweets=tweets) 50 | 51 | 52 | @app.route('/tweet', methods=['POST']) 53 | def tweet(): 54 | if g.user is None: 55 | return redirect(url_for('login', next=request.url)) 56 | status = request.form['tweet'] 57 | if not status: 58 | return redirect(url_for('index')) 59 | resp = twitter.post('statuses/update.json', data={ 60 | 'status': status 61 | }) 62 | 63 | if resp.status == 403: 64 | flash("Error: #%d, %s " % ( 65 | resp.data.get('errors')[0].get('code'), 66 | resp.data.get('errors')[0].get('message')) 67 | ) 68 | elif resp.status == 401: 69 | flash('Authorization error with Twitter.') 70 | else: 71 | flash('Successfully tweeted your tweet (ID: #%s)' % resp.data['id']) 72 | return redirect(url_for('index')) 73 | 74 | 75 | @app.route('/login') 76 | def login(): 77 | callback_url = url_for('oauthorized', next=request.args.get('next')) 78 | return twitter.authorize(callback=callback_url or request.referrer or None) 79 | 80 | 81 | @app.route('/logout') 82 | def logout(): 83 | session.pop('twitter_oauth', None) 84 | return redirect(url_for('index')) 85 | 86 | 87 | @app.route('/oauthorized') 88 | def oauthorized(): 89 | resp = twitter.authorized_response() 90 | if resp is None: 91 | flash('You denied the request to sign in.') 92 | else: 93 | session['twitter_oauth'] = resp 94 | return redirect(url_for('index')) 95 | 96 | 97 | if __name__ == '__main__': 98 | app.run() 99 | -------------------------------------------------------------------------------- /flask_oauthlib/contrib/cache.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from werkzeug.contrib.cache import NullCache, SimpleCache, FileSystemCache 4 | from werkzeug.contrib.cache import MemcachedCache, RedisCache 5 | 6 | 7 | class Cache(object): 8 | def __init__(self, app, config_prefix='OAUTHLIB', **kwargs): 9 | self.config_prefix = config_prefix 10 | self.config = app.config 11 | 12 | cache_type = '_%s' % self._config('type') 13 | kwargs.update(dict( 14 | default_timeout=self._config('DEFAULT_TIMEOUT', 100) 15 | )) 16 | 17 | try: 18 | self.cache = getattr(self, cache_type)(**kwargs) 19 | except AttributeError: 20 | raise RuntimeError( 21 | '`%s` is not a valid cache type!' % cache_type 22 | ) 23 | app.extensions[config_prefix.lower() + '_cache'] = self.cache 24 | 25 | def __getattr__(self, key): 26 | try: 27 | return object.__getattribute__(self, key) 28 | except AttributeError: 29 | try: 30 | return getattr(self.cache, key) 31 | except AttributeError: 32 | raise AttributeError('No such attribute: %s' % key) 33 | 34 | def _config(self, key, default='error'): 35 | key = key.upper() 36 | prior = '%s_CACHE_%s' % (self.config_prefix, key) 37 | if prior in self.config: 38 | return self.config[prior] 39 | fallback = 'CACHE_%s' % key 40 | if fallback in self.config: 41 | return self.config[fallback] 42 | if default == 'error': 43 | raise RuntimeError('%s is missing.' % prior) 44 | return default 45 | 46 | def _null(self, **kwargs): 47 | """Returns a :class:`NullCache` instance""" 48 | return NullCache() 49 | 50 | def _simple(self, **kwargs): 51 | """Returns a :class:`SimpleCache` instance 52 | 53 | .. warning:: 54 | 55 | This cache system might not be thread safe. Use with caution. 56 | """ 57 | kwargs.update(dict(threshold=self._config('threshold', 500))) 58 | return SimpleCache(**kwargs) 59 | 60 | def _memcache(self, **kwargs): 61 | """Returns a :class:`MemcachedCache` instance""" 62 | kwargs.update(dict( 63 | servers=self._config('MEMCACHED_SERVERS', None), 64 | key_prefix=self._config('key_prefix', None), 65 | )) 66 | return MemcachedCache(**kwargs) 67 | 68 | def _redis(self, **kwargs): 69 | """Returns a :class:`RedisCache` instance""" 70 | kwargs.update(dict( 71 | host=self._config('REDIS_HOST', 'localhost'), 72 | port=self._config('REDIS_PORT', 6379), 73 | password=self._config('REDIS_PASSWORD', None), 74 | db=self._config('REDIS_DB', 0), 75 | key_prefix=self._config('KEY_PREFIX', None), 76 | )) 77 | return RedisCache(**kwargs) 78 | 79 | def _filesystem(self, **kwargs): 80 | """Returns a :class:`FileSystemCache` instance""" 81 | kwargs.update(dict( 82 | threshold=self._config('threshold', 500), 83 | )) 84 | return FileSystemCache(self._config('dir', None), **kwargs) 85 | -------------------------------------------------------------------------------- /tests/test_oauth2/test_refresh.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from .base import TestCase 4 | from .base import create_server, sqlalchemy_provider, cache_provider 5 | from .base import db, Client, User, Token 6 | 7 | 8 | class TestDefaultProvider(TestCase): 9 | def create_server(self): 10 | create_server(self.app) 11 | 12 | def prepare_data(self): 13 | self.create_server() 14 | 15 | normal_client = Client( 16 | name='normal_client', 17 | client_id='normal_client', 18 | client_secret='normal_secret', 19 | is_confidential=False, 20 | _redirect_uris='http://localhost/authorized', 21 | ) 22 | 23 | confidential_client = Client( 24 | name='confidential_client', 25 | client_id='confidential_client', 26 | client_secret='confidential_secret', 27 | is_confidential=True, 28 | _redirect_uris='http://localhost/authorized', 29 | ) 30 | 31 | db.session.add(User(username='foo')) 32 | db.session.add(normal_client) 33 | db.session.add(confidential_client) 34 | db.session.commit() 35 | 36 | self.normal_client = normal_client 37 | self.confidential_client = confidential_client 38 | 39 | def test_normal_get_token(self): 40 | user = User.query.first() 41 | token = Token( 42 | user_id=user.id, 43 | client_id=self.normal_client.client_id, 44 | access_token='foo', 45 | refresh_token='bar', 46 | expires_in=1000, 47 | ) 48 | db.session.add(token) 49 | db.session.commit() 50 | 51 | rv = self.client.post('/oauth/token', data={ 52 | 'grant_type': 'refresh_token', 53 | 'refresh_token': token.refresh_token, 54 | 'client_id': self.normal_client.client_id, 55 | }) 56 | assert b'access_token' in rv.data 57 | 58 | def test_confidential_get_token(self): 59 | user = User.query.first() 60 | token = Token( 61 | user_id=user.id, 62 | client_id=self.confidential_client.client_id, 63 | access_token='foo', 64 | refresh_token='bar', 65 | expires_in=1000, 66 | ) 67 | db.session.add(token) 68 | db.session.commit() 69 | 70 | rv = self.client.post('/oauth/token', data={ 71 | 'grant_type': 'refresh_token', 72 | 'refresh_token': token.refresh_token, 73 | 'client_id': self.confidential_client.client_id, 74 | }) 75 | assert b'error' in rv.data 76 | 77 | rv = self.client.post('/oauth/token', data={ 78 | 'grant_type': 'refresh_token', 79 | 'refresh_token': token.refresh_token, 80 | 'client_id': self.confidential_client.client_id, 81 | 'client_secret': self.confidential_client.client_secret, 82 | }) 83 | assert b'access_token' in rv.data 84 | 85 | 86 | class TestSQLAlchemyProvider(TestDefaultProvider): 87 | def create_server(self): 88 | create_server(self.app, sqlalchemy_provider(self.app)) 89 | 90 | 91 | class TestCacheProvider(TestDefaultProvider): 92 | def create_server(self): 93 | create_server(self.app, cache_provider(self.app)) 94 | -------------------------------------------------------------------------------- /tests/test_oauth2/test_password.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from .base import TestCase 4 | from .base import create_server, sqlalchemy_provider, cache_provider 5 | from .base import db, Client, User 6 | 7 | 8 | class TestDefaultProvider(TestCase): 9 | def create_server(self): 10 | create_server(self.app) 11 | 12 | def prepare_data(self): 13 | self.create_server() 14 | 15 | oauth_client = Client( 16 | name='ios', client_id='pass-client', client_secret='pass-secret', 17 | _redirect_uris='http://localhost/authorized', 18 | ) 19 | 20 | db.session.add(User(username='foo')) 21 | db.session.add(oauth_client) 22 | db.session.commit() 23 | 24 | self.oauth_client = oauth_client 25 | 26 | def test_invalid_username(self): 27 | rv = self.client.post('/oauth/token', data={ 28 | 'grant_type': 'password', 29 | 'username': 'notfound', 30 | 'password': 'right', 31 | 'client_id': self.oauth_client.client_id, 32 | 'client_secret': self.oauth_client.client_secret, 33 | }) 34 | assert b'error' in rv.data 35 | 36 | def test_invalid_password(self): 37 | rv = self.client.post('/oauth/token', data={ 38 | 'grant_type': 'password', 39 | 'username': 'foo', 40 | 'password': 'wrong', 41 | 'client_id': self.oauth_client.client_id, 42 | 'client_secret': self.oauth_client.client_secret, 43 | }) 44 | assert b'error' in rv.data 45 | 46 | def test_missing_client_secret(self): 47 | rv = self.client.post('/oauth/token', data={ 48 | 'grant_type': 'password', 49 | 'username': 'foo', 50 | 'password': 'wrong', 51 | 'client_id': self.oauth_client.client_id, 52 | }) 53 | assert b'error' in rv.data 54 | 55 | def test_get_token(self): 56 | rv = self.client.post('/oauth/token', data={ 57 | 'grant_type': 'password', 58 | 'username': 'foo', 59 | 'password': 'right', 60 | 'client_id': self.oauth_client.client_id, 61 | 'client_secret': self.oauth_client.client_secret, 62 | }) 63 | assert b'access_token' in rv.data 64 | 65 | # in Authorization 66 | auth = 'cGFzcy1jbGllbnQ6cGFzcy1zZWNyZXQ=' 67 | rv = self.client.post('/oauth/token', data={ 68 | 'grant_type': 'password', 69 | 'username': 'foo', 70 | 'password': 'right', 71 | }, headers={'Authorization': 'Basic %s' % auth}) 72 | assert b'access_token' in rv.data 73 | 74 | def test_disallow_grant_type(self): 75 | self.oauth_client.disallow_grant_type = 'password' 76 | db.session.add(self.oauth_client) 77 | db.session.commit() 78 | 79 | rv = self.client.post('/oauth/token', data={ 80 | 'grant_type': 'password', 81 | 'username': 'foo', 82 | 'password': 'right', 83 | 'client_id': self.oauth_client.client_id, 84 | 'client_secret': self.oauth_client.client_secret, 85 | }) 86 | assert b'error' in rv.data 87 | 88 | 89 | class TestSQLAlchemyProvider(TestDefaultProvider): 90 | def create_server(self): 91 | create_server(self.app, sqlalchemy_provider(self.app)) 92 | 93 | 94 | class TestCacheProvider(TestDefaultProvider): 95 | def create_server(self): 96 | create_server(self.app, cache_provider(self.app)) 97 | -------------------------------------------------------------------------------- /flask_oauthlib/contrib/client/__init__.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from flask import current_app 4 | from werkzeug.local import LocalProxy 5 | 6 | from .application import OAuth1Application, OAuth2Application 7 | 8 | 9 | __all__ = ['OAuth', 'OAuth1Application', 'OAuth2Application'] 10 | 11 | 12 | class OAuth(object): 13 | """The extension to integrate OAuth 1.0a/2.0 to Flask applications. 14 | 15 | oauth = OAuth(app) 16 | 17 | or:: 18 | 19 | oauth = OAuth() 20 | oauth.init_app(app) 21 | """ 22 | 23 | state_key = 'oauthlib.contrib.client' 24 | 25 | def __init__(self, app=None): 26 | self.remote_apps = {} 27 | if app is not None: 28 | self.init_app(app) 29 | 30 | def init_app(self, app): 31 | app.extensions = getattr(app, 'extensions', {}) 32 | app.extensions[self.state_key] = OAuthState() 33 | 34 | def add_remote_app(self, remote_app, name=None, **kwargs): 35 | """Adds remote application and applies custom attributes on it. 36 | 37 | If the application instance's name is different from the argument 38 | provided name, or the keyword arguments is not empty, then the 39 | application instance will not be modified but be copied as a 40 | prototype. 41 | 42 | :param remote_app: the remote application instance. 43 | :type remote_app: the subclasses of :class:`BaseApplication` 44 | :params kwargs: the overriding attributes for the application instance. 45 | """ 46 | if name is None: 47 | name = remote_app.name 48 | if name != remote_app.name or kwargs: 49 | remote_app = copy.copy(remote_app) 50 | remote_app.name = name 51 | vars(remote_app).update(kwargs) 52 | if not hasattr(remote_app, 'clients'): 53 | remote_app.clients = cached_clients 54 | self.remote_apps[name] = remote_app 55 | return remote_app 56 | 57 | def remote_app(self, name, version=None, **kwargs): 58 | """Creates and adds new remote application. 59 | 60 | :param name: the remote application's name. 61 | :param version: '1' or '2', the version code of OAuth protocol. 62 | :param kwargs: the attributes of remote application. 63 | """ 64 | if version is None: 65 | if 'request_token_url' in kwargs: 66 | version = '1' 67 | else: 68 | version = '2' 69 | if version == '1': 70 | remote_app = OAuth1Application(name, clients=cached_clients) 71 | elif version == '2': 72 | remote_app = OAuth2Application(name, clients=cached_clients) 73 | else: 74 | raise ValueError('unkonwn version %r' % version) 75 | return self.add_remote_app(remote_app, **kwargs) 76 | 77 | def __getitem__(self, name): 78 | return self.remote_apps[name] 79 | 80 | def __getattr__(self, key): 81 | try: 82 | return object.__getattribute__(self, key) 83 | except AttributeError: 84 | app = self.remote_apps.get(key) 85 | if app: 86 | return app 87 | raise AttributeError('No such app: %s' % key) 88 | 89 | 90 | class OAuthState(object): 91 | 92 | def __init__(self): 93 | self.cached_clients = {} 94 | 95 | 96 | def get_cached_clients(): 97 | """Gets the cached clients dictionary in current context.""" 98 | if OAuth.state_key not in current_app.extensions: 99 | raise RuntimeError('%r is not initialized.' % current_app) 100 | state = current_app.extensions[OAuth.state_key] 101 | return state.cached_clients 102 | 103 | 104 | cached_clients = LocalProxy(get_cached_clients) 105 | -------------------------------------------------------------------------------- /docs/additional.rst: -------------------------------------------------------------------------------- 1 | Additional Features 2 | =================== 3 | 4 | This documentation covers some additional features. They are not required, 5 | but they may be very helpful. 6 | 7 | Request Hooks 8 | ------------- 9 | 10 | Like Flask, Flask-OAuthlib has before_request and after_request hooks too. 11 | It is usually useful for setting limitation on the client request with 12 | before_request:: 13 | 14 | @oauth.before_request 15 | def limit_client_request(): 16 | from flask_oauthlib.utils import extract_params 17 | uri, http_method, body, headers = extract_params() 18 | request = oauth._create_request(uri, http_method, body, headers) 19 | 20 | client_id = request.client_key 21 | if not client_id: 22 | return 23 | client = Client.get(client_id) 24 | if over_limit(client): 25 | return abort(403) 26 | 27 | track_request(client) 28 | 29 | And you can also modify the response with after_request:: 30 | 31 | @oauth.after_request 32 | def valid_after_request(valid, request): 33 | if request.user in black_list: 34 | return False, request 35 | return valid, oauth 36 | 37 | Bindings 38 | -------- 39 | 40 | .. versionchanged:: 0.4 41 | 42 | .. module:: flask_oauthlib.contrib.oauth2 43 | 44 | Bindings are objects you can use to configure flask-oauthlib for use with 45 | various data stores. They allow you to define the required getters and setters 46 | for each data store with little effort. 47 | 48 | SQLAlchemy OAuth2 49 | ````````````````` 50 | 51 | :meth:`bind_sqlalchemy` sets up getters and setters for storing the user, 52 | client, token and grant with SQLAlchemy, with some sane defaults. To use this 53 | class you'll need to create a SQLAlchemy model for each object. You can find 54 | examples of how to setup your SQLAlchemy models here: ref:`oauth2`. 55 | 56 | You'll also need to provide another function which returns the currently 57 | logged-in user. 58 | 59 | An example of how to use :meth:`bind_sqlalchemy`:: 60 | 61 | oauth = OAuth2Provider(app) 62 | 63 | bind_sqlalchemy(oauth, db.session, user=User, client=Client, 64 | token=Token, grant=Grant, current_user=current_user) 65 | 66 | Any of the classes can be omitted if you wish to register the getters and 67 | setters yourself:: 68 | 69 | oauth = OAuth2Provider(app) 70 | 71 | bind_sqlalchemy(oauth, db.session, user=User, client=Client, 72 | token=Token) 73 | 74 | @oauth.grantgetter 75 | def get_grant(client_id, code): 76 | pass 77 | 78 | @oauth.grantsetter 79 | def set_grant(client_id, code, request, *args, **kwargs): 80 | pass 81 | 82 | # register tokensetter with oauth but keeping the tokengetter 83 | # registered by `SQLAlchemyBinding` 84 | # You would only do this for the token and grant since user and client 85 | # only have getters 86 | @oauth.tokensetter 87 | def set_token(token, request, *args, **kwargs): 88 | pass 89 | 90 | `current_user` is only used with the Grant bindings, therefore if you are going 91 | to register your own grant getter and setter you don't need to provide that 92 | function. 93 | 94 | Grant Cache 95 | ``````````` 96 | 97 | Since the life of a Grant token is very short (usually about 100 seconds), 98 | storing it in a relational database is inefficient. 99 | The :meth:`bind_cache_grant` allows you to more efficiently cache the grant 100 | token using Memcache, Redis, or some other caching system. 101 | 102 | An example:: 103 | 104 | oauth = OAuth2Provider(app) 105 | app.config.update({'OAUTH2_CACHE_TYPE': 'redis'}) 106 | 107 | bind_cache_grant(app, oauth, current_user) 108 | 109 | - `app`: flask application 110 | - `oauth`: OAuth2Provider instance 111 | - `current_user`: a function that returns the current user 112 | 113 | The configuration options are described below. The :meth:`bind_cache_grant` 114 | will use the configuration options from `Flask-Cache` if they are set, else it 115 | will set them to the following defaults. Any configuration specific to 116 | :meth:`bind_cache_grant` will take precedence over any `Flask-Cache` 117 | configuration that has been set. 118 | -------------------------------------------------------------------------------- /example/reddit.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session, request 2 | from flask_oauthlib.client import OAuth, OAuthException, OAuthRemoteApp, parse_response 3 | from flask_oauthlib.utils import to_bytes 4 | import uuid 5 | import base64 6 | import time 7 | 8 | REDDIT_APP_ID = '6WnQXb-elQ3DLw' 9 | REDDIT_APP_SECRET = 'KzQickJEBxNHmt5bpO_HmSiupTw' 10 | # Reddit requires you to set nice User-Agent containing your username 11 | REDDIT_USER_AGENT = 'flask-oauthlib testing by /u/' 12 | 13 | app = Flask(__name__) 14 | app.debug = True 15 | app.secret_key = 'development' 16 | oauth = OAuth(app) 17 | 18 | 19 | class RedditOAuthRemoteApp(OAuthRemoteApp): 20 | def __init__(self, *args, **kwargs): 21 | super(RedditOAuthRemoteApp, self).__init__(*args, **kwargs) 22 | 23 | def handle_oauth2_response(self): 24 | if self.access_token_method != 'POST': 25 | raise OAuthException( 26 | 'Unsupported access_token_method: %s' % 27 | self.access_token_method 28 | ) 29 | 30 | client = self.make_client() 31 | remote_args = { 32 | 'code': request.args.get('code'), 33 | 'client_secret': self.consumer_secret, 34 | 'redirect_uri': session.get('%s_oauthredir' % self.name) 35 | } 36 | remote_args.update(self.access_token_params) 37 | 38 | reddit_basic_auth = base64.encodestring('%s:%s' % (REDDIT_APP_ID, REDDIT_APP_SECRET)).replace('\n', '') 39 | body = client.prepare_request_body(**remote_args) 40 | while True: 41 | resp, content = self.http_request( 42 | self.expand_url(self.access_token_url), 43 | headers={'Content-Type': 'application/x-www-form-urlencoded', 44 | 'Authorization': 'Basic %s' % reddit_basic_auth, 45 | 'User-Agent': REDDIT_USER_AGENT}, 46 | data=to_bytes(body, self.encoding), 47 | method=self.access_token_method, 48 | ) 49 | # Reddit API is rate-limited, so if we get 429, we need to retry 50 | if resp.code != 429: 51 | break 52 | time.sleep(1) 53 | 54 | data = parse_response(resp, content, content_type=self.content_type) 55 | if resp.code not in (200, 201): 56 | raise OAuthException( 57 | 'Invalid response from %s' % self.name, 58 | type='invalid_response', data=data 59 | ) 60 | return data 61 | 62 | reddit = RedditOAuthRemoteApp( 63 | oauth, 64 | 'reddit', 65 | consumer_key=REDDIT_APP_ID, 66 | consumer_secret=REDDIT_APP_SECRET, 67 | request_token_params={'scope': 'identity'}, 68 | base_url='https://oauth.reddit.com/api/v1/', 69 | request_token_url=None, 70 | access_token_url='https://www.reddit.com/api/v1/access_token', 71 | access_token_method='POST', 72 | authorize_url='https://www.reddit.com/api/v1/authorize' 73 | ) 74 | 75 | oauth.remote_apps['reddit'] = reddit 76 | 77 | 78 | @app.route('/') 79 | def index(): 80 | return redirect(url_for('login')) 81 | 82 | 83 | @app.route('/login') 84 | def login(): 85 | callback = url_for('reddit_authorized', _external=True) 86 | return reddit.authorize(callback=callback, state=uuid.uuid4()) 87 | 88 | 89 | @app.route('/login/authorized') 90 | def reddit_authorized(): 91 | resp = reddit.authorized_response() 92 | if isinstance(resp, OAuthException): 93 | print(resp.data) 94 | 95 | if resp is None: 96 | return 'Access denied: error=%s' % request.args['error'], 97 | session['reddit_oauth_token'] = (resp['access_token'], '') 98 | 99 | # This request may fail(429 Too Many Requests) 100 | # If you plan to use API heavily(and not just for auth), 101 | # it may be better to use PRAW: https://github.com/praw-dev/praw 102 | me = reddit.get('me') 103 | return 'Logged in as name=%s link_karma=%s comment_karma=%s' % \ 104 | (me.data['name'], me.data['link_karma'], me.data['comment_karma']) 105 | 106 | 107 | @reddit.tokengetter 108 | def get_reddit_oauth_token(): 109 | return session.get('reddit_oauth_token') 110 | 111 | 112 | if __name__ == '__main__': 113 | app.run() 114 | -------------------------------------------------------------------------------- /example/qq.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import json 4 | from flask import Flask, redirect, url_for, session, request, jsonify, Markup 5 | from flask_oauthlib.client import OAuth 6 | 7 | # get yours at http://connect.qq.com 8 | QQ_APP_ID = os.getenv('QQ_APP_ID', '101187283') 9 | QQ_APP_KEY = os.getenv('QQ_APP_KEY', '993983549da49e384d03adfead8b2489') 10 | 11 | app = Flask(__name__) 12 | app.debug = True 13 | app.secret_key = 'development' 14 | oauth = OAuth(app) 15 | 16 | qq = oauth.remote_app( 17 | 'qq', 18 | consumer_key=QQ_APP_ID, 19 | consumer_secret=QQ_APP_KEY, 20 | base_url='https://graph.qq.com', 21 | request_token_url=None, 22 | request_token_params={'scope': 'get_user_info'}, 23 | access_token_url='/oauth2.0/token', 24 | authorize_url='/oauth2.0/authorize', 25 | ) 26 | 27 | 28 | def json_to_dict(x): 29 | '''OAuthResponse class can't parse the JSON data with content-type 30 | - text/html and because of a rubbish api, we can't just tell flask-oauthlib to treat it as json.''' 31 | if x.find(b'callback') > -1: 32 | # the rubbish api (https://graph.qq.com/oauth2.0/authorize) is handled here as special case 33 | pos_lb = x.find(b'{') 34 | pos_rb = x.find(b'}') 35 | x = x[pos_lb:pos_rb + 1] 36 | 37 | try: 38 | if type(x) != str: # Py3k 39 | x = x.decode('utf-8') 40 | return json.loads(x, encoding='utf-8') 41 | except: 42 | return x 43 | 44 | 45 | def update_qq_api_request_data(data={}): 46 | '''Update some required parameters for OAuth2.0 API calls''' 47 | defaults = { 48 | 'openid': session.get('qq_openid'), 49 | 'access_token': session.get('qq_token')[0], 50 | 'oauth_consumer_key': QQ_APP_ID, 51 | } 52 | defaults.update(data) 53 | return defaults 54 | 55 | 56 | @app.route('/') 57 | def index(): 58 | '''just for verify website owner here.''' 59 | return Markup('''''') 61 | 62 | 63 | @app.route('/user_info') 64 | def get_user_info(): 65 | if 'qq_token' in session: 66 | data = update_qq_api_request_data() 67 | resp = qq.get('/user/get_user_info', data=data) 68 | return jsonify(status=resp.status, data=json_to_dict(resp.data)) 69 | return redirect(url_for('login')) 70 | 71 | 72 | @app.route('/login') 73 | def login(): 74 | return qq.authorize(callback=url_for('authorized', _external=True)) 75 | 76 | 77 | @app.route('/logout') 78 | def logout(): 79 | session.pop('qq_token', None) 80 | return redirect(url_for('get_user_info')) 81 | 82 | 83 | @app.route('/login/authorized') 84 | def authorized(): 85 | resp = qq.authorized_response() 86 | if resp is None: 87 | return 'Access denied: reason=%s error=%s' % ( 88 | request.args['error_reason'], 89 | request.args['error_description'] 90 | ) 91 | session['qq_token'] = (resp['access_token'], '') 92 | 93 | # Get openid via access_token, openid and access_token are needed for API calls 94 | resp = qq.get('/oauth2.0/me', {'access_token': session['qq_token'][0]}) 95 | resp = json_to_dict(resp.data) 96 | if isinstance(resp, dict): 97 | session['qq_openid'] = resp.get('openid') 98 | 99 | return redirect(url_for('get_user_info')) 100 | 101 | 102 | @qq.tokengetter 103 | def get_qq_oauth_token(): 104 | return session.get('qq_token') 105 | 106 | 107 | def convert_keys_to_string(dictionary): 108 | '''Recursively converts dictionary keys to strings.''' 109 | if not isinstance(dictionary, dict): 110 | return dictionary 111 | return dict((str(k), convert_keys_to_string(v)) for k, v in dictionary.items()) 112 | 113 | 114 | def change_qq_header(uri, headers, body): 115 | '''On SAE platform, when headers' keys are unicode type, will raise 116 | ``HTTP Error 400: Bad request``, so need convert keys from unicode to str. 117 | Otherwise, ignored it.''' 118 | # uncomment below line while deploy on SAE platform 119 | # headers = convert_keys_to_string(headers) 120 | return uri, headers, body 121 | 122 | qq.pre_request = change_qq_header 123 | 124 | 125 | if __name__ == '__main__': 126 | app.run() 127 | -------------------------------------------------------------------------------- /tests/test_oauth2/test_code.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from datetime import datetime, timedelta 4 | from .base import TestCase 5 | from .base import create_server, sqlalchemy_provider, cache_provider 6 | from .base import db, Client, User, Grant 7 | 8 | 9 | class TestDefaultProvider(TestCase): 10 | def create_server(self): 11 | create_server(self.app) 12 | 13 | def prepare_data(self): 14 | self.create_server() 15 | 16 | oauth_client = Client( 17 | name='ios', client_id='code-client', client_secret='code-secret', 18 | _redirect_uris='http://localhost/authorized', 19 | ) 20 | 21 | db.session.add(User(username='foo')) 22 | db.session.add(oauth_client) 23 | db.session.commit() 24 | 25 | self.oauth_client = oauth_client 26 | self.authorize_url = ( 27 | '/oauth/authorize?response_type=code&client_id=%s' 28 | ) % oauth_client.client_id 29 | 30 | def test_get_authorize(self): 31 | rv = self.client.get('/oauth/authorize') 32 | assert 'client_id' in rv.location 33 | 34 | rv = self.client.get('/oauth/authorize?client_id=no') 35 | assert 'client_id' in rv.location 36 | 37 | url = '/oauth/authorize?client_id=%s' % self.oauth_client.client_id 38 | rv = self.client.get(url) 39 | assert 'error' in rv.location 40 | 41 | rv = self.client.get(self.authorize_url) 42 | assert b'confirm' in rv.data 43 | 44 | def test_post_authorize(self): 45 | url = self.authorize_url + '&scope=foo' 46 | rv = self.client.post(url, data={'confirm': 'yes'}) 47 | assert 'invalid_scope' in rv.location 48 | 49 | url = self.authorize_url + '&scope=email' 50 | rv = self.client.post(url, data={'confirm': 'yes'}) 51 | assert 'code' in rv.location 52 | 53 | url = self.authorize_url + '&scope=' 54 | rv = self.client.post(url, data={'confirm': 'yes'}) 55 | assert 'error=Scopes+must+be+set' in rv.location 56 | 57 | def test_invalid_token(self): 58 | rv = self.client.get('/oauth/token') 59 | assert b'unsupported_grant_type' in rv.data 60 | 61 | rv = self.client.get('/oauth/token?grant_type=authorization_code') 62 | assert b'error' in rv.data 63 | assert b'code' in rv.data 64 | 65 | url = ( 66 | '/oauth/token?grant_type=authorization_code' 67 | '&code=nothing&client_id=%s' 68 | ) % self.oauth_client.client_id 69 | rv = self.client.get(url) 70 | assert b'invalid_client' in rv.data 71 | 72 | url += '&client_secret=' + self.oauth_client.client_secret 73 | rv = self.client.get(url) 74 | assert b'invalid_client' not in rv.data 75 | assert rv.status_code == 401 76 | 77 | def test_invalid_redirect_uri(self): 78 | authorize_url = ( 79 | '/oauth/authorize?response_type=code&client_id=dev' 80 | '&redirect_uri=http://localhost:8000/authorized' 81 | '&scope=invalid' 82 | ) 83 | rv = self.client.get(authorize_url) 84 | assert 'error=' in rv.location 85 | assert 'trying+to+decode+a+non+urlencoded+string' in rv.location 86 | 87 | def test_get_token(self): 88 | expires = datetime.utcnow() + timedelta(seconds=100) 89 | grant = Grant( 90 | user_id=1, 91 | client_id=self.oauth_client.client_id, 92 | scope='email', 93 | redirect_uri='http://localhost/authorized', 94 | code='test-get-token', 95 | expires=expires, 96 | ) 97 | db.session.add(grant) 98 | db.session.commit() 99 | 100 | url = ( 101 | '/oauth/token?grant_type=authorization_code' 102 | '&code=test-get-token&client_id=%s' 103 | ) % self.oauth_client.client_id 104 | rv = self.client.get(url) 105 | assert b'invalid_client' in rv.data 106 | 107 | url += '&client_secret=' + self.oauth_client.client_secret 108 | rv = self.client.get(url) 109 | assert b'access_token' in rv.data 110 | 111 | 112 | class TestSQLAlchemyProvider(TestDefaultProvider): 113 | def create_server(self): 114 | create_server(self.app, sqlalchemy_provider(self.app)) 115 | 116 | 117 | class TestCacheProvider(TestDefaultProvider): 118 | def create_server(self): 119 | create_server(self.app, cache_provider(self.app)) 120 | 121 | def test_get_token(self): 122 | url = self.authorize_url + '&scope=email' 123 | rv = self.client.post(url, data={'confirm': 'yes'}) 124 | assert 'code' in rv.location 125 | code = rv.location.split('code=')[1] 126 | 127 | url = ( 128 | '/oauth/token?grant_type=authorization_code' 129 | '&code=%s&client_id=%s' 130 | ) % (code, self.oauth_client.client_id) 131 | rv = self.client.get(url) 132 | assert b'invalid_client' in rv.data 133 | 134 | url += '&client_secret=' + self.oauth_client.client_secret 135 | rv = self.client.get(url) 136 | assert b'access_token' in rv.data 137 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-OAuthlib.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-OAuthlib.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-OAuthlib" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-OAuthlib" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /tests/oauth1/test_oauth1.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import time 4 | from mock import MagicMock 5 | from nose.tools import raises 6 | from flask import Flask 7 | from flask_oauthlib.client import OAuth, OAuthException 8 | from .server import create_server, db 9 | from .client import create_client 10 | from .._base import BaseSuite, clean_url 11 | from .._base import to_unicode as u 12 | 13 | 14 | class OAuthSuite(BaseSuite): 15 | @property 16 | def database(self): 17 | return db 18 | 19 | def create_app(self): 20 | app = Flask(__name__) 21 | app.debug = True 22 | app.testing = True 23 | app.secret_key = 'development' 24 | return app 25 | 26 | def setup_app(self, app): 27 | self.create_server(app) 28 | client = self.create_client(app) 29 | client.http_request = MagicMock( 30 | side_effect=self.patch_request(app) 31 | ) 32 | 33 | def create_server(self, app): 34 | create_server(app) 35 | return app 36 | 37 | def create_client(self, app): 38 | return create_client(app) 39 | 40 | 41 | class TestWebAuth(OAuthSuite): 42 | def test_full_flow(self): 43 | rv = self.client.get('/login') 44 | assert 'oauth_token' in rv.location 45 | 46 | auth_url = clean_url(rv.location) 47 | rv = self.client.get(auth_url) 48 | assert '' in u(rv.data) 49 | 50 | rv = self.client.post(auth_url, data={ 51 | 'confirm': 'yes' 52 | }) 53 | assert 'oauth_token' in rv.location 54 | 55 | token_url = clean_url(rv.location) 56 | rv = self.client.get(token_url) 57 | assert 'oauth_token_secret' in u(rv.data) 58 | 59 | rv = self.client.get('/') 60 | assert 'email' in u(rv.data) 61 | 62 | rv = self.client.get('/address') 63 | assert rv.status_code == 401 64 | 65 | rv = self.client.get('/method/post') 66 | assert 'POST' in u(rv.data) 67 | 68 | rv = self.client.get('/method/put') 69 | assert 'PUT' in u(rv.data) 70 | 71 | rv = self.client.get('/method/delete') 72 | assert 'DELETE' in u(rv.data) 73 | 74 | def test_no_confirm(self): 75 | rv = self.client.get('/login') 76 | assert 'oauth_token' in rv.location 77 | 78 | auth_url = clean_url(rv.location) 79 | rv = self.client.post(auth_url, data={ 80 | 'confirm': 'no' 81 | }) 82 | assert 'error=denied' in rv.location 83 | 84 | def test_invalid_request_token(self): 85 | rv = self.client.get('/login') 86 | assert 'oauth_token' in rv.location 87 | loc = rv.location.replace('oauth_token=', 'oauth_token=a') 88 | 89 | auth_url = clean_url(loc) 90 | rv = self.client.get(auth_url) 91 | assert 'error' in rv.location 92 | 93 | rv = self.client.post(auth_url, data={ 94 | 'confirm': 'yes' 95 | }) 96 | assert 'error' in rv.location 97 | 98 | 99 | auth_header = ( 100 | u'OAuth realm="%(realm)s",' 101 | u'oauth_nonce="97392753692390970531372987366",' 102 | u'oauth_timestamp="%(timestamp)d", oauth_version="1.0",' 103 | u'oauth_signature_method="%(signature_method)s",' 104 | u'oauth_consumer_key="%(key)s",' 105 | u'oauth_callback="%(callback)s",' 106 | u'oauth_signature="%(signature)s"' 107 | ) 108 | auth_dict = { 109 | 'realm': 'email', 110 | 'timestamp': int(time.time()), 111 | 'key': 'dev', 112 | 'signature_method': 'HMAC-SHA1', 113 | 'callback': 'http%3A%2F%2Flocalhost%2Fauthorized', 114 | 'signature': 'LngsvwVPnd8vCZ2hr7umJvqb%2Fyw%3D', 115 | } 116 | 117 | 118 | class TestInvalid(OAuthSuite): 119 | @raises(OAuthException) 120 | def test_request(self): 121 | self.client.get('/login') 122 | 123 | def test_request_token(self): 124 | rv = self.client.get('/oauth/request_token') 125 | assert 'error' in u(rv.data) 126 | 127 | def test_access_token(self): 128 | rv = self.client.get('/oauth/access_token') 129 | assert 'error' in u(rv.data) 130 | 131 | def test_invalid_realms(self): 132 | auth_format = auth_dict.copy() 133 | auth_format['realm'] = 'profile' 134 | 135 | headers = { 136 | u'Authorization': auth_header % auth_format 137 | } 138 | rv = self.client.get('/oauth/request_token', headers=headers) 139 | assert 'error' in u(rv.data) 140 | assert 'realm' in u(rv.data) 141 | 142 | def test_no_realms(self): 143 | auth_format = auth_dict.copy() 144 | auth_format['realm'] = '' 145 | 146 | headers = { 147 | u'Authorization': auth_header % auth_format 148 | } 149 | rv = self.client.get('/oauth/request_token', headers=headers) 150 | assert 'secret' in u(rv.data) 151 | 152 | def test_no_callback(self): 153 | auth_format = auth_dict.copy() 154 | auth_format['callback'] = '' 155 | 156 | headers = { 157 | u'Authorization': auth_header % auth_format 158 | } 159 | rv = self.client.get('/oauth/request_token', headers=headers) 160 | assert 'error' in u(rv.data) 161 | assert 'callback' in u(rv.data) 162 | 163 | def test_invalid_signature_method(self): 164 | auth_format = auth_dict.copy() 165 | auth_format['signature_method'] = 'PLAIN' 166 | 167 | headers = { 168 | u'Authorization': auth_header % auth_format 169 | } 170 | rv = self.client.get('/oauth/request_token', headers=headers) 171 | assert 'error' in u(rv.data) 172 | assert 'signature' in u(rv.data) 173 | 174 | def create_client(self, app): 175 | oauth = OAuth(app) 176 | 177 | remote = oauth.remote_app( 178 | 'dev', 179 | consumer_key='noclient', 180 | consumer_secret='dev', 181 | request_token_params={'realm': 'email'}, 182 | base_url='http://localhost/api/', 183 | request_token_url='http://localhost/oauth/request_token', 184 | access_token_method='GET', 185 | access_token_url='http://localhost/oauth/access_token', 186 | authorize_url='http://localhost/oauth/authorize' 187 | ) 188 | return create_client(app, remote) 189 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from nose.tools import raises 3 | from flask_oauthlib.client import encode_request_data 4 | from flask_oauthlib.client import OAuthRemoteApp, OAuth 5 | from flask_oauthlib.client import parse_response 6 | 7 | try: 8 | import urllib2 as http 9 | http_urlopen = 'urllib2.urlopen' 10 | except ImportError: 11 | from urllib import request as http 12 | http_urlopen = 'urllib.request.urlopen' 13 | 14 | from mock import patch 15 | 16 | 17 | class Response(object): 18 | def __init__(self, content, headers=None): 19 | self.content = content 20 | self.headers = headers or {} 21 | 22 | @property 23 | def code(self): 24 | return self.headers.get('status-code', 500) 25 | 26 | @property 27 | def status_code(self): 28 | return self.code 29 | 30 | def read(self): 31 | return self.content 32 | 33 | def close(self): 34 | return self 35 | 36 | 37 | def test_encode_request_data(): 38 | data, _ = encode_request_data('foo', None) 39 | assert data == 'foo' 40 | 41 | data, f = encode_request_data(None, 'json') 42 | assert data == '{}' 43 | assert f == 'application/json' 44 | 45 | data, f = encode_request_data(None, 'urlencoded') 46 | assert data == '' 47 | assert f == 'application/x-www-form-urlencoded' 48 | 49 | 50 | def test_app(): 51 | app = Flask(__name__) 52 | oauth = OAuth(app) 53 | remote = oauth.remote_app( 54 | 'dev', 55 | consumer_key='dev', 56 | consumer_secret='dev', 57 | request_token_params={'scope': 'email'}, 58 | base_url='http://127.0.0.1:5000/api/', 59 | request_token_url=None, 60 | access_token_method='POST', 61 | access_token_url='http://127.0.0.1:5000/oauth/token', 62 | authorize_url='http://127.0.0.1:5000/oauth/authorize' 63 | ) 64 | client = app.extensions['oauthlib.client'] 65 | assert client.dev.name == 'dev' 66 | 67 | 68 | def test_parse_xml(): 69 | resp = Response( 70 | 'bar', headers={ 71 | 'status-code': 200, 72 | 'content-type': 'text/xml' 73 | } 74 | ) 75 | parse_response(resp, resp.read()) 76 | 77 | 78 | @raises(AttributeError) 79 | def test_raise_app(): 80 | app = Flask(__name__) 81 | oauth = OAuth(app) 82 | client = app.extensions['oauthlib.client'] 83 | assert client.demo.name == 'dev' 84 | 85 | 86 | class TestOAuthRemoteApp(object): 87 | @raises(TypeError) 88 | def test_raise_init(self): 89 | OAuthRemoteApp('oauth', 'twitter') 90 | 91 | def test_not_raise_init(self): 92 | OAuthRemoteApp('oauth', 'twitter', app_key='foo') 93 | 94 | def test_lazy_load(self): 95 | oauth = OAuth() 96 | twitter = oauth.remote_app( 97 | 'twitter', 98 | base_url='https://api.twitter.com/1/', 99 | app_key='twitter' 100 | ) 101 | assert twitter.base_url == 'https://api.twitter.com/1/' 102 | 103 | app = Flask(__name__) 104 | app.config.update({ 105 | 'twitter': dict( 106 | request_token_params={'realms': 'email'}, 107 | consumer_key='twitter key', 108 | consumer_secret='twitter secret', 109 | request_token_url='request url', 110 | access_token_url='token url', 111 | authorize_url='auth url', 112 | ) 113 | }) 114 | oauth.init_app(app) 115 | assert twitter.consumer_key == 'twitter key' 116 | assert twitter.consumer_secret == 'twitter secret' 117 | assert twitter.request_token_url == 'request url' 118 | assert twitter.access_token_url == 'token url' 119 | assert twitter.authorize_url == 'auth url' 120 | assert twitter.content_type is None 121 | assert 'realms' in twitter.request_token_params 122 | 123 | def test_lazy_load_with_plain_text_config(self): 124 | oauth = OAuth() 125 | twitter = oauth.remote_app('twitter', app_key='TWITTER') 126 | 127 | app = Flask(__name__) 128 | app.config['TWITTER_CONSUMER_KEY'] = 'twitter key' 129 | app.config['TWITTER_CONSUMER_SECRET'] = 'twitter secret' 130 | app.config['TWITTER_REQUEST_TOKEN_URL'] = 'request url' 131 | app.config['TWITTER_ACCESS_TOKEN_URL'] = 'token url' 132 | app.config['TWITTER_AUTHORIZE_URL'] = 'auth url' 133 | 134 | oauth.init_app(app) 135 | 136 | assert twitter.consumer_key == 'twitter key' 137 | assert twitter.consumer_secret == 'twitter secret' 138 | assert twitter.request_token_url == 'request url' 139 | assert twitter.access_token_url == 'token url' 140 | assert twitter.authorize_url == 'auth url' 141 | 142 | @patch(http_urlopen) 143 | def test_http_request(self, urlopen): 144 | urlopen.return_value = Response( 145 | b'{"foo": "bar"}', headers={'status-code': 200} 146 | ) 147 | 148 | resp, content = OAuthRemoteApp.http_request('http://example.com') 149 | assert resp.code == 200 150 | assert b'foo' in content 151 | 152 | resp, content = OAuthRemoteApp.http_request( 153 | 'http://example.com/', 154 | method='GET', 155 | data={'wd': 'flask-oauthlib'} 156 | ) 157 | assert resp.code == 200 158 | assert b'foo' in content 159 | 160 | resp, content = OAuthRemoteApp.http_request( 161 | 'http://example.com/', 162 | data={'wd': 'flask-oauthlib'} 163 | ) 164 | assert resp.code == 200 165 | assert b'foo' in content 166 | 167 | @patch(http_urlopen) 168 | def test_raise_http_request(self, urlopen): 169 | error = http.HTTPError( 170 | 'http://example.com/', 404, 'Not Found', None, None 171 | ) 172 | error.read = lambda: b'o' 173 | 174 | class _Fake(object): 175 | def close(self): 176 | return 0 177 | 178 | class _Faker(object): 179 | _closer = _Fake() 180 | 181 | error.file = _Faker() 182 | 183 | urlopen.side_effect = error 184 | resp, content = OAuthRemoteApp.http_request('http://example.com') 185 | assert resp.code == 404 186 | assert b'o' in content 187 | -------------------------------------------------------------------------------- /tests/oauth1/server.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from flask import g, render_template, request, jsonify 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_oauthlib.provider import OAuth1Provider 5 | 6 | 7 | db = SQLAlchemy() 8 | 9 | 10 | def enable_log(name='flask_oauthlib'): 11 | import logging 12 | logger = logging.getLogger(name) 13 | logger.addHandler(logging.StreamHandler()) 14 | logger.setLevel(logging.DEBUG) 15 | 16 | 17 | # enable_log() 18 | 19 | 20 | class User(db.Model): 21 | id = db.Column(db.Integer, primary_key=True) 22 | username = db.Column(db.String(40), unique=True, index=True, 23 | nullable=False) 24 | 25 | 26 | class Client(db.Model): 27 | # id = db.Column(db.Integer, primary_key=True) 28 | # human readable name 29 | client_key = db.Column(db.String(40), primary_key=True) 30 | client_secret = db.Column(db.String(55), unique=True, index=True, 31 | nullable=False) 32 | rsa_key = db.Column(db.String(55)) 33 | _realms = db.Column(db.Text) 34 | _redirect_uris = db.Column(db.Text) 35 | 36 | @property 37 | def user(self): 38 | return User.query.get(1) 39 | 40 | @property 41 | def redirect_uris(self): 42 | if self._redirect_uris: 43 | return self._redirect_uris.split() 44 | return [] 45 | 46 | @property 47 | def default_redirect_uri(self): 48 | return self.redirect_uris[0] 49 | 50 | @property 51 | def default_realms(self): 52 | if self._realms: 53 | return self._realms.split() 54 | return [] 55 | 56 | 57 | class Grant(db.Model): 58 | id = db.Column(db.Integer, primary_key=True) 59 | user_id = db.Column( 60 | db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') 61 | ) 62 | user = db.relationship('User') 63 | 64 | client_key = db.Column( 65 | db.String(40), db.ForeignKey('client.client_key'), 66 | nullable=False, 67 | ) 68 | client = db.relationship('Client') 69 | 70 | token = db.Column(db.String(255), index=True, unique=True) 71 | secret = db.Column(db.String(255), nullable=False) 72 | 73 | verifier = db.Column(db.String(255)) 74 | 75 | expires = db.Column(db.DateTime) 76 | redirect_uri = db.Column(db.Text) 77 | _realms = db.Column(db.Text) 78 | 79 | def delete(self): 80 | db.session.delete(self) 81 | db.session.commit() 82 | return self 83 | 84 | @property 85 | def realms(self): 86 | if self._realms: 87 | return self._realms.split() 88 | return [] 89 | 90 | 91 | class Token(db.Model): 92 | id = db.Column(db.Integer, primary_key=True) 93 | client_key = db.Column( 94 | db.String(40), db.ForeignKey('client.client_key'), 95 | nullable=False, 96 | ) 97 | client = db.relationship('Client') 98 | 99 | user_id = db.Column( 100 | db.Integer, db.ForeignKey('user.id'), 101 | ) 102 | user = db.relationship('User') 103 | 104 | token = db.Column(db.String(255)) 105 | secret = db.Column(db.String(255)) 106 | 107 | _realms = db.Column(db.Text) 108 | 109 | @property 110 | def realms(self): 111 | if self._realms: 112 | return self._realms.split() 113 | return [] 114 | 115 | 116 | def prepare_app(app): 117 | db.init_app(app) 118 | db.app = app 119 | db.create_all() 120 | 121 | client1 = Client( 122 | client_key='dev', client_secret='dev', 123 | _redirect_uris=( 124 | 'http://localhost:8000/authorized ' 125 | 'http://localhost/authorized' 126 | ), 127 | _realms='email', 128 | ) 129 | 130 | user = User(username='admin') 131 | 132 | try: 133 | db.session.add(client1) 134 | db.session.add(user) 135 | db.session.commit() 136 | except: 137 | db.session.rollback() 138 | return app 139 | 140 | 141 | def create_server(app): 142 | app = prepare_app(app) 143 | 144 | oauth = OAuth1Provider(app) 145 | 146 | @oauth.clientgetter 147 | def get_client(client_key): 148 | return Client.query.filter_by(client_key=client_key).first() 149 | 150 | @oauth.tokengetter 151 | def load_access_token(client_key, token, *args, **kwargs): 152 | t = Token.query.filter_by(client_key=client_key, token=token).first() 153 | return t 154 | 155 | @oauth.tokensetter 156 | def save_access_token(token, req): 157 | tok = Token( 158 | client_key=req.client.client_key, 159 | user_id=req.user.id, 160 | token=token['oauth_token'], 161 | secret=token['oauth_token_secret'], 162 | _realms=token['oauth_authorized_realms'], 163 | ) 164 | db.session.add(tok) 165 | db.session.commit() 166 | 167 | @oauth.grantgetter 168 | def load_request_token(token): 169 | grant = Grant.query.filter_by(token=token).first() 170 | return grant 171 | 172 | @oauth.grantsetter 173 | def save_request_token(token, oauth): 174 | if oauth.realms: 175 | realms = ' '.join(oauth.realms) 176 | else: 177 | realms = None 178 | grant = Grant( 179 | token=token['oauth_token'], 180 | secret=token['oauth_token_secret'], 181 | client_key=oauth.client.client_key, 182 | redirect_uri=oauth.redirect_uri, 183 | _realms=realms, 184 | ) 185 | db.session.add(grant) 186 | db.session.commit() 187 | return grant 188 | 189 | @oauth.verifiergetter 190 | def load_verifier(verifier, token): 191 | return Grant.query.filter_by(verifier=verifier, token=token).first() 192 | 193 | @oauth.verifiersetter 194 | def save_verifier(token, verifier, *args, **kwargs): 195 | tok = Grant.query.filter_by(token=token).first() 196 | tok.verifier = verifier['oauth_verifier'] 197 | tok.user_id = g.user.id 198 | db.session.add(tok) 199 | db.session.commit() 200 | return tok 201 | 202 | @oauth.noncegetter 203 | def load_nonce(*args, **kwargs): 204 | return None 205 | 206 | @oauth.noncesetter 207 | def save_nonce(*args, **kwargs): 208 | return None 209 | 210 | @app.before_request 211 | def load_current_user(): 212 | user = User.query.get(1) 213 | g.user = user 214 | 215 | @app.route('/home') 216 | def home(): 217 | return render_template('home.html') 218 | 219 | @app.route('/oauth/authorize', methods=['GET', 'POST']) 220 | @oauth.authorize_handler 221 | def authorize(*args, **kwargs): 222 | # NOTICE: for real project, you need to require login 223 | if request.method == 'GET': 224 | # render a page for user to confirm the authorization 225 | return render_template('confirm.html') 226 | 227 | confirm = request.form.get('confirm', 'no') 228 | return confirm == 'yes' 229 | 230 | @app.route('/oauth/request_token') 231 | @oauth.request_token_handler 232 | def request_token(): 233 | return {} 234 | 235 | @app.route('/oauth/access_token') 236 | @oauth.access_token_handler 237 | def access_token(): 238 | return {} 239 | 240 | @app.route('/api/email') 241 | @oauth.require_oauth('email') 242 | def email_api(): 243 | oauth = request.oauth 244 | return jsonify(email='me@oauth.net', username=oauth.user.username) 245 | 246 | @app.route('/api/address/') 247 | @oauth.require_oauth('address') 248 | def address_api(city): 249 | oauth = request.oauth 250 | return jsonify(address=city, username=oauth.user.username) 251 | 252 | @app.route('/api/method', methods=['GET', 'POST', 'PUT', 'DELETE']) 253 | @oauth.require_oauth() 254 | def method_api(): 255 | return jsonify(method=request.method) 256 | 257 | return app 258 | 259 | 260 | if __name__ == '__main__': 261 | from flask import Flask 262 | app = Flask(__name__) 263 | app.debug = True 264 | app.secret_key = 'development' 265 | app.config.update({ 266 | 'SQLALCHEMY_DATABASE_URI': 'sqlite:///oauth1.sqlite', 267 | 'OAUTH1_PROVIDER_ENFORCE_SSL': False, 268 | 'OAUTH1_PROVIDER_KEY_LENGTH': (3, 30), 269 | 'OAUTH1_PROVIDER_REALMS': ['email', 'address'] 270 | }) 271 | app = create_server(app) 272 | app.run() 273 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Here you can see the full list of changes between each Flask-OAuthlib release. 5 | 6 | Version 0.9.3 7 | ------------- 8 | 9 | Released on Jun 2, 2016 10 | 11 | - Revert the wrong implement of non credential oauth2 require auth 12 | - Catch all exceptions in OAuth2 providers 13 | - Bugfix for examples, docs and other things 14 | 15 | 16 | Version 0.9.2 17 | ------------- 18 | 19 | Released on Nov 3, 2015 20 | 21 | - Bugfix in client parse_response when body is none. 22 | - Update contrib client by @tonyseek 23 | - Typo fix for OAuth1 provider 24 | - Fix OAuth2 provider on non credential clients by @Fleurer 25 | 26 | 27 | Version 0.9.1 28 | ------------- 29 | 30 | Released on Mar 9, 2015 31 | 32 | - Improve on security. 33 | - Fix on contrib client. 34 | 35 | Version 0.9.0 36 | ------------- 37 | 38 | Released on Feb 3, 2015 39 | 40 | - New feature for contrib client, which will become the official client in 41 | the future via `#136`_ and `#176`_. 42 | - Add appropriate headers when making POST request for access toke via `#169`_. 43 | - Use a local copy of instance 'request_token_params' attribute to avoid side 44 | effects via `#177`_. 45 | - Some minor fixes of contrib by Hsiaoming Yang. 46 | 47 | .. _`#177`: https://github.com/lepture/flask-oauthlib/pull/177 48 | .. _`#169`: https://github.com/lepture/flask-oauthlib/pull/169 49 | .. _`#136`: https://github.com/lepture/flask-oauthlib/pull/136 50 | .. _`#176`: https://github.com/lepture/flask-oauthlib/pull/176 51 | 52 | 53 | Version 0.8.0 54 | ------------- 55 | 56 | Released on Dec 3, 2014 57 | 58 | .. module:: flask_oauthlib.provider.oauth2 59 | 60 | - New feature for generating refresh tokens 61 | - Add new function :meth:`OAuth2Provider.verify_request` for non vanilla Flask projects 62 | - Some small bugfixes 63 | 64 | 65 | Version 0.7.0 66 | ------------- 67 | 68 | Released on Aug 20, 2014 69 | 70 | .. module:: flask_oauthlib.client 71 | 72 | - Deprecated :meth:`OAuthRemoteApp.authorized_handler` in favor of 73 | :meth:`OAuthRemoteApp.authorized_response`. 74 | - Add revocation endpoint via `#131`_. 75 | - Handle unknown exceptions in providers. 76 | - Add PATCH method for client via `#134`_. 77 | 78 | .. _`#131`: https://github.com/lepture/flask-oauthlib/pull/131 79 | .. _`#134`: https://github.com/lepture/flask-oauthlib/pull/134 80 | 81 | 82 | Version 0.6.0 83 | ------------- 84 | 85 | Released on Jul 29, 2014 86 | 87 | - Compatible with OAuthLib 0.6.2 and 0.6.3 88 | - Add invalid_response decorator to handle invalid request 89 | - Add error_message for OAuthLib Request. 90 | 91 | Version 0.5.0 92 | ------------- 93 | 94 | Released on May 13, 2014 95 | 96 | - Add ``contrib.apps`` module, thanks for tonyseek via `#94`_. 97 | - Status code changed to 401 for invalid access token via `#93`_. 98 | - **Security bug** for access token via `#92`_. 99 | - Fix for client part, request token params for OAuth1 via `#91`_. 100 | - **API change** for ``oauth.require_oauth`` via `#89`_. 101 | - Fix for OAuth2 provider, support client authentication for authorization-code grant type via `#86`_. 102 | - Fix client_credentials logic in validate_grant_type via `#85`_. 103 | - Fix for client part, pass access token method via `#83`_. 104 | - Fix for OAuth2 provider related to confidential client via `#82`_. 105 | 106 | Upgrade From 0.4.x to 0.5.0 107 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 108 | 109 | API for OAuth providers ``oauth.require_oauth`` has changed. 110 | 111 | Before the change, you would write code like:: 112 | 113 | @app.route('/api/user') 114 | @oauth.require_oauth('email') 115 | def user(req): 116 | return jsonify(req.user) 117 | 118 | After the change, you would write code like:: 119 | 120 | from flask import request 121 | 122 | @app.route('/api/user') 123 | @oauth.require_oauth('email') 124 | def user(): 125 | return jsonify(request.oauth.user) 126 | 127 | .. _`#94`: https://github.com/lepture/flask-oauthlib/pull/94 128 | .. _`#93`: https://github.com/lepture/flask-oauthlib/issues/93 129 | .. _`#92`: https://github.com/lepture/flask-oauthlib/issues/92 130 | .. _`#91`: https://github.com/lepture/flask-oauthlib/issues/91 131 | .. _`#89`: https://github.com/lepture/flask-oauthlib/issues/89 132 | .. _`#86`: https://github.com/lepture/flask-oauthlib/pull/86 133 | .. _`#85`: https://github.com/lepture/flask-oauthlib/pull/85 134 | .. _`#83`: https://github.com/lepture/flask-oauthlib/pull/83 135 | .. _`#82`: https://github.com/lepture/flask-oauthlib/issues/82 136 | 137 | Thanks Stian Prestholdt and Jiangge Zhang. 138 | 139 | Version 0.4.3 140 | ------------- 141 | 142 | Released on Feb 18, 2014 143 | 144 | - OAuthlib released 0.6.1, which caused a bug in oauth2 provider. 145 | - Validation for scopes on oauth2 right via `#72`_. 146 | - Handle empty response for application/json via `#69`_. 147 | 148 | .. _`#69`: https://github.com/lepture/flask-oauthlib/issues/69 149 | .. _`#72`: https://github.com/lepture/flask-oauthlib/issues/72 150 | 151 | Version 0.4.2 152 | ------------- 153 | 154 | Released on Jan 3, 2014 155 | 156 | Happy New Year! 157 | 158 | - Add param ``state`` in authorize method via `#63`_. 159 | - Bugfix for encoding error in Python 3 via `#65`_. 160 | 161 | .. _`#63`: https://github.com/lepture/flask-oauthlib/issues/63 162 | .. _`#65`: https://github.com/lepture/flask-oauthlib/issues/65 163 | 164 | Version 0.4.1 165 | ------------- 166 | 167 | Released on Nov 25, 2013 168 | 169 | - Add access_token on request object via `#53`_. 170 | - Bugfix for lazy loading configuration via `#55`_. 171 | 172 | .. _`#53`: https://github.com/lepture/flask-oauthlib/issues/53 173 | .. _`#55`: https://github.com/lepture/flask-oauthlib/issues/55 174 | 175 | 176 | Version 0.4.0 177 | ------------- 178 | 179 | Released on Nov 12, 2013 180 | 181 | - Redesign contrib library. 182 | - A new way for lazy loading configuration via `#51`_. 183 | - Some bugfixes. 184 | 185 | .. _`#51`: https://github.com/lepture/flask-oauthlib/issues/51 186 | 187 | 188 | Version 0.3.4 189 | ------------- 190 | 191 | Released on Oct 31, 2013 192 | 193 | - Bugfix for client missing a string placeholder via `#49`_. 194 | - Bugfix for client property getter via `#48`_. 195 | 196 | .. _`#49`: https://github.com/lepture/flask-oauthlib/issues/49 197 | .. _`#48`: https://github.com/lepture/flask-oauthlib/issues/48 198 | 199 | Version 0.3.3 200 | ------------- 201 | 202 | Released on Oct 4, 2013 203 | 204 | - Support for token generator in OAuth2 Provider via `#42`_. 205 | - Improve client part, improve test cases. 206 | - Fix scope via `#44`_. 207 | 208 | .. _`#42`: https://github.com/lepture/flask-oauthlib/issues/42 209 | .. _`#44`: https://github.com/lepture/flask-oauthlib/issues/44 210 | 211 | Version 0.3.2 212 | ------------- 213 | 214 | Released on Sep 13, 2013 215 | 216 | - Upgrade oauthlib to 0.6 217 | - A quick bugfix for request token params via `#40`_. 218 | 219 | .. _`#40`: https://github.com/lepture/flask-oauthlib/issues/40 220 | 221 | Version 0.3.1 222 | ------------- 223 | 224 | Released on Aug 22, 2013 225 | 226 | - Add contrib module via `#15`_. We are still working on it, 227 | take your own risk. 228 | - Add example of linkedin via `#35`_. 229 | - Compatible with new proposals of oauthlib. 230 | - Bugfix for client part. 231 | - Backward compatible for lower version of Flask via `#37`_. 232 | 233 | .. _`#15`: https://github.com/lepture/flask-oauthlib/issues/15 234 | .. _`#35`: https://github.com/lepture/flask-oauthlib/issues/35 235 | .. _`#37`: https://github.com/lepture/flask-oauthlib/issues/37 236 | 237 | Version 0.3.0 238 | ------------- 239 | 240 | Released on July 10, 2013. 241 | 242 | - OAuth1 Provider available. Documentation at :doc:`oauth1`. :) 243 | - Add ``before_request`` and ``after_request`` via `#22`_. 244 | - Lazy load configuration for client via `#23`_. Documentation at :ref:`lazy-configuration`. 245 | - Python 3 compatible now. 246 | 247 | .. _`#22`: https://github.com/lepture/flask-oauthlib/issues/22 248 | .. _`#23`: https://github.com/lepture/flask-oauthlib/issues/23 249 | 250 | Version 0.2.0 251 | ------------- 252 | 253 | Released on June 19, 2013. 254 | 255 | - OAuth2 Provider available. Documentation at :doc:`oauth2`. :) 256 | - Make client part testable. 257 | - Change extension name of client from ``oauth-client`` to ``oauthlib.client``. 258 | 259 | Version 0.1.1 260 | ------------- 261 | 262 | Released on May 23, 2013. 263 | 264 | - Fix setup.py 265 | 266 | Version 0.1.0 267 | ------------- 268 | 269 | First public preview release on May 18, 2013. 270 | -------------------------------------------------------------------------------- /flask_oauthlib/contrib/apps.py: -------------------------------------------------------------------------------- 1 | """ 2 | flask_oauthlib.contrib.apps 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | The bundle of remote app factories for famous third platforms. 6 | 7 | Usage:: 8 | 9 | from flask import Flask 10 | from flask_oauthlib.client import OAuth 11 | from flask_oauthlib.contrib.apps import github 12 | 13 | app = Flask(__name__) 14 | oauth = OAuth(app) 15 | 16 | github.register_to(oauth, scope=['user:email']) 17 | github.register_to(oauth, name='github2') 18 | 19 | Of course, it requires consumer keys in your config:: 20 | 21 | GITHUB_CONSUMER_KEY = '' 22 | GITHUB_CONSUMER_SECRET = '' 23 | GITHUB2_CONSUMER_KEY = '' 24 | GITHUB2_CONSUMER_SECRET = '' 25 | 26 | Some apps with OAuth 1.0a such as Twitter could not accept the ``scope`` 27 | argument. 28 | 29 | Contributed by: tonyseek 30 | """ 31 | 32 | import copy 33 | 34 | from oauthlib.common import unicode_type, bytes_type 35 | 36 | 37 | __all__ = ['douban', 'dropbox', 'facebook', 'github', 'google', 'linkedin', 38 | 'twitter', 'weibo'] 39 | 40 | 41 | class RemoteAppFactory(object): 42 | """The factory to create remote app and bind it to given extension. 43 | 44 | :param default_name: the default name which be used for registering. 45 | :param kwargs: the pre-defined kwargs. 46 | :param docstring: the docstring of factory. 47 | """ 48 | 49 | def __init__(self, default_name, kwargs, docstring=''): 50 | assert 'name' not in kwargs 51 | assert 'register' not in kwargs 52 | self.default_name = default_name 53 | self.kwargs = kwargs 54 | self._kwargs_processor = None 55 | self.__doc__ = docstring.lstrip() 56 | 57 | def register_to(self, oauth, name=None, **kwargs): 58 | """Creates a remote app and registers it.""" 59 | kwargs = self._process_kwargs( 60 | name=(name or self.default_name), **kwargs) 61 | return oauth.remote_app(**kwargs) 62 | 63 | def create(self, oauth, **kwargs): 64 | """Creates a remote app only.""" 65 | kwargs = self._process_kwargs( 66 | name=self.default_name, register=False, **kwargs) 67 | return oauth.remote_app(**kwargs) 68 | 69 | def kwargs_processor(self, fn): 70 | """Sets a function to process kwargs before creating any app.""" 71 | self._kwargs_processor = fn 72 | return fn 73 | 74 | def _process_kwargs(self, **kwargs): 75 | final_kwargs = copy.deepcopy(self.kwargs) 76 | # merges with pre-defined kwargs 77 | final_kwargs.update(copy.deepcopy(kwargs)) 78 | # use name as app key 79 | final_kwargs.setdefault('app_key', final_kwargs['name'].upper()) 80 | # processes by pre-defined function 81 | if self._kwargs_processor is not None: 82 | final_kwargs = self._kwargs_processor(**final_kwargs) 83 | return final_kwargs 84 | 85 | 86 | def make_scope_processor(default_scope): 87 | def processor(**kwargs): 88 | # request_token_params 89 | scope = kwargs.pop('scope', [default_scope]) # default scope 90 | if not isinstance(scope, (unicode_type, bytes_type)): 91 | scope = ','.join(scope) # allows list-style scope 92 | request_token_params = kwargs.setdefault('request_token_params', {}) 93 | request_token_params.setdefault('scope', scope) # doesn't override 94 | return kwargs 95 | return processor 96 | 97 | 98 | douban = RemoteAppFactory('douban', { 99 | 'base_url': 'https://api.douban.com/v2/', 100 | 'request_token_url': None, 101 | 'access_token_url': 'https://www.douban.com/service/auth2/token', 102 | 'authorize_url': 'https://www.douban.com/service/auth2/auth', 103 | 'access_token_method': 'POST', 104 | }, """ 105 | The OAuth app for douban.com API. 106 | 107 | :param scope: optional. default: ``['douban_basic_common']``. 108 | see also: http://developers.douban.com/wiki/?title=oauth2 109 | """) 110 | douban.kwargs_processor(make_scope_processor('douban_basic_common')) 111 | 112 | 113 | dropbox = RemoteAppFactory('dropbox', { 114 | 'base_url': 'https://www.dropbox.com/1/', 115 | 'request_token_url': None, 116 | 'access_token_url': 'https://api.dropbox.com/1/oauth2/token', 117 | 'authorize_url': 'https://www.dropbox.com/1/oauth2/authorize', 118 | 'access_token_method': 'POST', 119 | 'request_token_params': {}, 120 | }, """The OAuth app for Dropbox API.""") 121 | 122 | 123 | facebook = RemoteAppFactory('facebook', { 124 | 'request_token_params': {'scope': 'email'}, 125 | 'base_url': 'https://graph.facebook.com', 126 | 'request_token_url': None, 127 | 'access_token_url': '/oauth/access_token', 128 | 'authorize_url': 'https://www.facebook.com/dialog/oauth', 129 | }, """ 130 | The OAuth app for Facebook API. 131 | 132 | :param scope: optional. default: ``['email']``. 133 | """) 134 | facebook.kwargs_processor(make_scope_processor('email')) 135 | 136 | 137 | github = RemoteAppFactory('github', { 138 | 'base_url': 'https://api.github.com/', 139 | 'request_token_url': None, 140 | 'access_token_method': 'POST', 141 | 'access_token_url': 'https://github.com/login/oauth/access_token', 142 | 'authorize_url': 'https://github.com/login/oauth/authorize', 143 | }, """ 144 | The OAuth app for GitHub API. 145 | 146 | :param scope: optional. default: ``['user:email']``. 147 | """) 148 | github.kwargs_processor(make_scope_processor('user:email')) 149 | 150 | 151 | google = RemoteAppFactory('google', { 152 | 'base_url': 'https://www.googleapis.com/oauth2/v1/', 153 | 'request_token_url': None, 154 | 'access_token_method': 'POST', 155 | 'access_token_url': 'https://accounts.google.com/o/oauth2/token', 156 | 'authorize_url': 'https://accounts.google.com/o/oauth2/auth', 157 | }, """ 158 | The OAuth app for Google API. 159 | 160 | :param scope: optional. 161 | default: ``['email']``. 162 | """) 163 | google.kwargs_processor(make_scope_processor( 164 | 'email')) 165 | 166 | 167 | twitter = RemoteAppFactory('twitter', { 168 | 'base_url': 'https://api.twitter.com/1.1/', 169 | 'request_token_url': 'https://api.twitter.com/oauth/request_token', 170 | 'access_token_url': 'https://api.twitter.com/oauth/access_token', 171 | 'authorize_url': 'https://api.twitter.com/oauth/authenticate', 172 | }, """The OAuth app for Twitter API.""") 173 | 174 | 175 | weibo = RemoteAppFactory('weibo', { 176 | 'base_url': 'https://api.weibo.com/2/', 177 | 'authorize_url': 'https://api.weibo.com/oauth2/authorize', 178 | 'request_token_url': None, 179 | 'access_token_method': 'POST', 180 | 'access_token_url': 'https://api.weibo.com/oauth2/access_token', 181 | # since weibo's response is a shit, we need to force parse the content 182 | 'content_type': 'application/json', 183 | }, """ 184 | The OAuth app for weibo.com API. 185 | 186 | :param scope: optional. default: ``['email']`` 187 | """) 188 | weibo.kwargs_processor(make_scope_processor('email')) 189 | 190 | 191 | def change_weibo_header(uri, headers, body): 192 | """Since weibo is a rubbish server, it does not follow the standard, 193 | we need to change the authorization header for it.""" 194 | auth = headers.get('Authorization') 195 | if auth: 196 | auth = auth.replace('Bearer', 'OAuth2') 197 | headers['Authorization'] = auth 198 | return uri, headers, body 199 | 200 | weibo.pre_request = change_weibo_header 201 | 202 | 203 | linkedin = RemoteAppFactory('linkedin', { 204 | 'request_token_params': {'state': 'RandomString'}, 205 | 'base_url': 'https://api.linkedin.com/v1/', 206 | 'request_token_url': None, 207 | 'access_token_method': 'POST', 208 | 'access_token_url': 'https://www.linkedin.com/uas/oauth2/accessToken', 209 | 'authorize_url': 'https://www.linkedin.com/uas/oauth2/authorization', 210 | }, """ 211 | The OAuth app for LinkedIn API. 212 | 213 | :param scope: optional. default: ``['r_basicprofile']`` 214 | """) 215 | linkedin.kwargs_processor(make_scope_processor('r_basicprofile')) 216 | 217 | 218 | def change_linkedin_query(uri, headers, body): 219 | auth = headers.pop('Authorization') 220 | headers['x-li-format'] = 'json' 221 | if auth: 222 | auth = auth.replace('Bearer', '').strip() 223 | if '?' in uri: 224 | uri += '&oauth2_access_token=' + auth 225 | else: 226 | uri += '?oauth2_access_token=' + auth 227 | return uri, headers, body 228 | 229 | linkedin.pre_request = change_linkedin_query 230 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask-OAuthlib documentation build configuration file, created by 4 | # sphinx-quickstart on Fri May 17 21:54:48 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | sys.path.append(os.path.abspath('_themes')) 16 | sys.path.append(os.path.abspath('.')) 17 | sys.path.append(os.path.abspath('..')) 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ----------------------------------------------------- 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be extensions 30 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 31 | extensions = ['sphinx.ext.autodoc'] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = '.rst' 38 | 39 | # The encoding of source files. 40 | #source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = 'index' 44 | 45 | # General information about the project. 46 | project = u'Flask-OAuthlib' 47 | 48 | import datetime 49 | copyright = u'2013 - %i, Hsiaoming Yang' % datetime.datetime.utcnow().year 50 | 51 | # The version info for the project you're documenting, acts as replacement for 52 | # |version| and |release|, also used in various other places throughout the 53 | # built documents. 54 | # 55 | # The short X.Y version. 56 | import flask_oauthlib 57 | version = flask_oauthlib.__version__ 58 | # The full version, including alpha/beta/rc tags. 59 | release = flask_oauthlib.__version__ 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | #language = None 64 | 65 | # There are two options for replacing |today|: either, you set today to some 66 | # non-false value, then it is used: 67 | #today = '' 68 | # Else, today_fmt is used as the format for a strftime call. 69 | #today_fmt = '%B %d, %Y' 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | exclude_patterns = ['_build'] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all documents. 76 | #default_role = None 77 | 78 | # If true, '()' will be appended to :func: etc. cross-reference text. 79 | #add_function_parentheses = True 80 | 81 | # If true, the current module name will be prepended to all description 82 | # unit titles (such as .. function::). 83 | #add_module_names = True 84 | 85 | # If true, sectionauthor and moduleauthor directives will be shown in the 86 | # output. They are ignored by default. 87 | #show_authors = False 88 | 89 | # The name of the Pygments (syntax highlighting) style to use. 90 | pygments_style = 'sphinx' 91 | 92 | # A list of ignored prefixes for module index sorting. 93 | #modindex_common_prefix = [] 94 | 95 | 96 | # -- Options for HTML output --------------------------------------------------- 97 | 98 | # The theme to use for HTML and HTML Help pages. See the documentation for 99 | # a list of builtin themes. 100 | html_theme = 'flask' 101 | 102 | # Theme options are theme-specific and customize the look and feel of a theme 103 | # further. For a list of options available for each theme, see the 104 | # documentation. 105 | #html_theme_options = {} 106 | 107 | # Add any paths that contain custom themes here, relative to this directory. 108 | html_theme_path = ['_themes'] 109 | 110 | # The name for this set of Sphinx documents. If None, it defaults to 111 | # " v documentation". 112 | #html_title = None 113 | 114 | # A shorter title for the navigation bar. Default is the same as html_title. 115 | #html_short_title = None 116 | 117 | # The name of an image file (relative to this directory) to place at the top 118 | # of the sidebar. 119 | html_logo = 'flask-oauthlib.png' 120 | 121 | # The name of an image file (within the static path) to use as favicon of the 122 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 123 | # pixels large. 124 | #html_favicon = None 125 | 126 | # Add any paths that contain custom static files (such as style sheets) here, 127 | # relative to this directory. They are copied after the builtin static files, 128 | # so a file named "default.css" will overwrite the builtin "default.css". 129 | html_static_path = ['_static'] 130 | 131 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 132 | # using the given strftime format. 133 | #html_last_updated_fmt = '%b %d, %Y' 134 | 135 | # If true, SmartyPants will be used to convert quotes and dashes to 136 | # typographically correct entities. 137 | #html_use_smartypants = True 138 | 139 | # Custom sidebar templates, maps document names to template names. 140 | html_sidebars = { 141 | 'index': ['brand.html', 'sidebarintro.html', 'searchbox.html'], 142 | '**': ['brand.html', 'localtoc.html', 'relations.html', 'searchbox.html'] 143 | } 144 | 145 | # Additional templates that should be rendered to pages, maps page names to 146 | # template names. 147 | #html_additional_pages = {} 148 | 149 | # If false, no module index is generated. 150 | #html_domain_indices = True 151 | 152 | # If false, no index is generated. 153 | #html_use_index = True 154 | 155 | # If true, the index is split into individual pages for each letter. 156 | #html_split_index = False 157 | 158 | # If true, links to the reST sources are added to the pages. 159 | html_show_sourcelink = False 160 | 161 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 162 | #html_show_sphinx = True 163 | 164 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 165 | #html_show_copyright = True 166 | 167 | # If true, an OpenSearch description file will be output, and all pages will 168 | # contain a tag referring to it. The value of this option must be the 169 | # base URL from which the finished HTML is served. 170 | #html_use_opensearch = '' 171 | 172 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 173 | #html_file_suffix = None 174 | 175 | # Output file base name for HTML help builder. 176 | htmlhelp_basename = 'Flask-OAuthlibdoc' 177 | 178 | 179 | # -- Options for LaTeX output -------------------------------------------------- 180 | 181 | latex_elements = { 182 | # The paper size ('letterpaper' or 'a4paper'). 183 | #'papersize': 'letterpaper', 184 | 185 | # The font size ('10pt', '11pt' or '12pt'). 186 | #'pointsize': '10pt', 187 | 188 | # Additional stuff for the LaTeX preamble. 189 | #'preamble': '', 190 | } 191 | 192 | # Grouping the document tree into LaTeX files. List of tuples 193 | # (source start file, target name, title, author, documentclass [howto/manual]). 194 | latex_documents = [ 195 | ('index', 'Flask-OAuthlib.tex', u'Flask-OAuthlib Documentation', 196 | u'Hsiaoming Yang', 'manual'), 197 | ] 198 | 199 | # The name of an image file (relative to this directory) to place at the top of 200 | # the title page. 201 | #latex_logo = None 202 | 203 | # For "manual" documents, if this is true, then toplevel headings are parts, 204 | # not chapters. 205 | #latex_use_parts = False 206 | 207 | # If true, show page references after internal links. 208 | #latex_show_pagerefs = False 209 | 210 | # If true, show URL addresses after external links. 211 | #latex_show_urls = False 212 | 213 | # Documents to append as an appendix to all manuals. 214 | #latex_appendices = [] 215 | 216 | # If false, no module index is generated. 217 | #latex_domain_indices = True 218 | 219 | 220 | # -- Options for manual page output -------------------------------------------- 221 | 222 | # One entry per manual page. List of tuples 223 | # (source start file, name, description, authors, manual section). 224 | man_pages = [ 225 | ('index', 'flask-oauthlib', u'Flask-OAuthlib Documentation', 226 | [u'Hsiaoming Yang'], 1) 227 | ] 228 | 229 | # If true, show URL addresses after external links. 230 | #man_show_urls = False 231 | 232 | 233 | # -- Options for Texinfo output ------------------------------------------------ 234 | 235 | # Grouping the document tree into Texinfo files. List of tuples 236 | # (source start file, target name, title, author, 237 | # dir menu entry, description, category) 238 | texinfo_documents = [ 239 | ('index', 'Flask-OAuthlib', u'Flask-OAuthlib Documentation', 240 | u'Hsiaoming Yang', 'Flask-OAuthlib', 'One line description of project.', 241 | 'Miscellaneous'), 242 | ] 243 | 244 | # Documents to append as an appendix to all manuals. 245 | #texinfo_appendices = [] 246 | 247 | # If false, no module index is generated. 248 | #texinfo_domain_indices = True 249 | 250 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 251 | #texinfo_show_urls = 'footnote' 252 | -------------------------------------------------------------------------------- /tests/test_oauth2/base.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import unittest 5 | from datetime import datetime, timedelta 6 | from flask import Flask 7 | from flask import g, render_template, request, jsonify, make_response 8 | from flask_sqlalchemy import SQLAlchemy 9 | from sqlalchemy.orm import relationship 10 | from flask_oauthlib.provider import OAuth2Provider 11 | from flask_oauthlib.contrib.oauth2 import bind_sqlalchemy 12 | from flask_oauthlib.contrib.oauth2 import bind_cache_grant 13 | 14 | os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = 'true' 15 | 16 | db = SQLAlchemy() 17 | 18 | 19 | class User(db.Model): 20 | id = db.Column(db.Integer, primary_key=True) 21 | username = db.Column(db.String(40), unique=True, index=True, 22 | nullable=False) 23 | 24 | def check_password(self, password): 25 | return password != 'wrong' 26 | 27 | 28 | class Client(db.Model): 29 | # id = db.Column(db.Integer, primary_key=True) 30 | # human readable name 31 | name = db.Column(db.String(40)) 32 | client_id = db.Column(db.String(40), primary_key=True) 33 | client_secret = db.Column(db.String(55), unique=True, index=True, 34 | nullable=False) 35 | _redirect_uris = db.Column(db.Text) 36 | default_scope = db.Column(db.Text, default='email address') 37 | disallow_grant_type = db.Column(db.String(20)) 38 | is_confidential = db.Column(db.Boolean, default=True) 39 | 40 | @property 41 | def user(self): 42 | return User.query.get(1) 43 | 44 | @property 45 | def redirect_uris(self): 46 | if self._redirect_uris: 47 | return self._redirect_uris.split() 48 | return [] 49 | 50 | @property 51 | def default_redirect_uri(self): 52 | return self.redirect_uris[0] 53 | 54 | @property 55 | def default_scopes(self): 56 | if self.default_scope: 57 | return self.default_scope.split() 58 | return [] 59 | 60 | @property 61 | def allowed_grant_types(self): 62 | types = [ 63 | 'authorization_code', 'password', 64 | 'client_credentials', 'refresh_token', 65 | ] 66 | if self.disallow_grant_type: 67 | types.remove(self.disallow_grant_type) 68 | return types 69 | 70 | 71 | class Grant(db.Model): 72 | id = db.Column(db.Integer, primary_key=True) 73 | user_id = db.Column( 74 | db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') 75 | ) 76 | user = relationship('User') 77 | 78 | client_id = db.Column( 79 | db.String(40), db.ForeignKey('client.client_id', ondelete='CASCADE'), 80 | nullable=False, 81 | ) 82 | client = relationship('Client') 83 | code = db.Column(db.String(255), index=True, nullable=False) 84 | 85 | redirect_uri = db.Column(db.String(255)) 86 | scope = db.Column(db.Text) 87 | expires = db.Column(db.DateTime) 88 | 89 | def delete(self): 90 | db.session.delete(self) 91 | db.session.commit() 92 | return self 93 | 94 | @property 95 | def scopes(self): 96 | if self.scope: 97 | return self.scope.split() 98 | return None 99 | 100 | 101 | class Token(db.Model): 102 | id = db.Column(db.Integer, primary_key=True) 103 | client_id = db.Column( 104 | db.String(40), db.ForeignKey('client.client_id', ondelete='CASCADE'), 105 | nullable=False, 106 | ) 107 | user_id = db.Column( 108 | db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') 109 | ) 110 | user = relationship('User') 111 | client = relationship('Client') 112 | token_type = db.Column(db.String(40)) 113 | access_token = db.Column(db.String(255)) 114 | refresh_token = db.Column(db.String(255)) 115 | expires = db.Column(db.DateTime) 116 | scope = db.Column(db.Text) 117 | 118 | def __init__(self, **kwargs): 119 | expires_in = kwargs.pop('expires_in') 120 | self.expires = datetime.utcnow() + timedelta(seconds=expires_in) 121 | for k, v in kwargs.items(): 122 | setattr(self, k, v) 123 | 124 | @property 125 | def scopes(self): 126 | if self.scope: 127 | return self.scope.split() 128 | return [] 129 | 130 | def delete(self): 131 | db.session.delete(self) 132 | db.session.commit() 133 | return self 134 | 135 | 136 | def current_user(): 137 | return g.user 138 | 139 | 140 | def cache_provider(app): 141 | oauth = OAuth2Provider(app) 142 | 143 | bind_sqlalchemy(oauth, db.session, user=User, 144 | token=Token, client=Client, current_user=current_user) 145 | 146 | app.config.update({'OAUTH2_CACHE_TYPE': 'simple'}) 147 | bind_cache_grant(app, oauth, current_user) 148 | return oauth 149 | 150 | 151 | def sqlalchemy_provider(app): 152 | oauth = OAuth2Provider(app) 153 | 154 | bind_sqlalchemy(oauth, db.session, user=User, token=Token, 155 | client=Client, grant=Grant, current_user=current_user) 156 | 157 | return oauth 158 | 159 | 160 | def default_provider(app): 161 | oauth = OAuth2Provider(app) 162 | 163 | @oauth.clientgetter 164 | def get_client(client_id): 165 | return Client.query.filter_by(client_id=client_id).first() 166 | 167 | @oauth.grantgetter 168 | def get_grant(client_id, code): 169 | return Grant.query.filter_by(client_id=client_id, code=code).first() 170 | 171 | @oauth.tokengetter 172 | def get_token(access_token=None, refresh_token=None): 173 | if access_token: 174 | return Token.query.filter_by(access_token=access_token).first() 175 | if refresh_token: 176 | return Token.query.filter_by(refresh_token=refresh_token).first() 177 | return None 178 | 179 | @oauth.grantsetter 180 | def set_grant(client_id, code, request, *args, **kwargs): 181 | expires = datetime.utcnow() + timedelta(seconds=100) 182 | grant = Grant( 183 | client_id=client_id, 184 | code=code['code'], 185 | redirect_uri=request.redirect_uri, 186 | scope=' '.join(request.scopes), 187 | user_id=g.user.id, 188 | expires=expires, 189 | ) 190 | db.session.add(grant) 191 | db.session.commit() 192 | 193 | @oauth.tokensetter 194 | def set_token(token, request, *args, **kwargs): 195 | # In real project, a token is unique bound to user and client. 196 | # Which means, you don't need to create a token every time. 197 | tok = Token(**token) 198 | if request.response_type == 'token': 199 | tok.user_id = g.user.id 200 | else: 201 | tok.user_id = request.user.id 202 | tok.client_id = request.client.client_id 203 | db.session.add(tok) 204 | db.session.commit() 205 | 206 | @oauth.usergetter 207 | def get_user(username, password, *args, **kwargs): 208 | # This is optional, if you don't need password credential 209 | # there is no need to implement this method 210 | user = User.query.filter_by(username=username).first() 211 | if user and user.check_password(password): 212 | return user 213 | return None 214 | 215 | return oauth 216 | 217 | 218 | def create_server(app, oauth=None): 219 | if not oauth: 220 | oauth = default_provider(app) 221 | 222 | @app.before_request 223 | def load_current_user(): 224 | user = User.query.get(1) 225 | g.user = user 226 | 227 | @app.route('/home') 228 | def home(): 229 | return render_template('home.html') 230 | 231 | @app.route('/oauth/authorize', methods=['GET', 'POST']) 232 | @oauth.authorize_handler 233 | def authorize(*args, **kwargs): 234 | # NOTICE: for real project, you need to require login 235 | if request.method == 'GET': 236 | # render a page for user to confirm the authorization 237 | return 'confirm page' 238 | 239 | if request.method == 'HEAD': 240 | # if HEAD is supported properly, request parameters like 241 | # client_id should be validated the same way as for 'GET' 242 | response = make_response('', 200) 243 | response.headers['X-Client-ID'] = kwargs.get('client_id') 244 | return response 245 | 246 | confirm = request.form.get('confirm', 'no') 247 | return confirm == 'yes' 248 | 249 | @app.route('/oauth/token', methods=['POST', 'GET']) 250 | @oauth.token_handler 251 | def access_token(): 252 | return {} 253 | 254 | @app.route('/oauth/revoke', methods=['POST']) 255 | @oauth.revoke_handler 256 | def revoke_token(): 257 | return {} 258 | 259 | @app.route('/api/email') 260 | @oauth.require_oauth('email') 261 | def email_api(): 262 | oauth = request.oauth 263 | return jsonify(email='me@oauth.net', username=oauth.user.username) 264 | 265 | @app.route('/api/client') 266 | @oauth.require_oauth() 267 | def client_api(): 268 | oauth = request.oauth 269 | return jsonify(client=oauth.client.name) 270 | 271 | @app.route('/api/address/') 272 | @oauth.require_oauth('address') 273 | def address_api(city): 274 | oauth = request.oauth 275 | return jsonify(address=city, username=oauth.user.username) 276 | 277 | @app.route('/api/method', methods=['GET', 'POST', 'PUT', 'DELETE']) 278 | @oauth.require_oauth() 279 | def method_api(): 280 | return jsonify(method=request.method) 281 | 282 | @oauth.invalid_response 283 | def require_oauth_invalid(req): 284 | return jsonify(message=req.error_message), 401 285 | 286 | return app 287 | 288 | 289 | class TestCase(unittest.TestCase): 290 | def setUp(self): 291 | app = self.create_app() 292 | 293 | app.testing = True 294 | self._ctx = app.app_context() 295 | self._ctx.push() 296 | 297 | db.init_app(app) 298 | db.create_all() 299 | 300 | self.app = app 301 | self.client = app.test_client() 302 | self.prepare_data() 303 | 304 | def tearDown(self): 305 | db.drop_all() 306 | self._ctx.pop() 307 | 308 | def prepare_data(self): 309 | return True 310 | 311 | def create_app(self): 312 | app = Flask(__name__) 313 | app.debug = True 314 | app.secret_key = 'testing' 315 | app.config.update({ 316 | 'SQLALCHEMY_DATABASE_URI': 'sqlite://' 317 | }) 318 | return app 319 | -------------------------------------------------------------------------------- /tests/oauth2/server.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from datetime import datetime, timedelta 3 | from flask import g, render_template, request, jsonify, make_response 4 | from flask.ext.sqlalchemy import SQLAlchemy 5 | from sqlalchemy.orm import relationship 6 | from flask_oauthlib.provider import OAuth2Provider 7 | from flask_oauthlib.contrib.oauth2 import bind_sqlalchemy 8 | from flask_oauthlib.contrib.oauth2 import bind_cache_grant 9 | 10 | 11 | db = SQLAlchemy() 12 | 13 | 14 | class User(db.Model): 15 | id = db.Column(db.Integer, primary_key=True) 16 | username = db.Column(db.String(40), unique=True, index=True, 17 | nullable=False) 18 | 19 | def check_password(self, password): 20 | return True 21 | 22 | 23 | class Client(db.Model): 24 | # id = db.Column(db.Integer, primary_key=True) 25 | # human readable name 26 | name = db.Column(db.String(40)) 27 | client_id = db.Column(db.String(40), primary_key=True) 28 | client_secret = db.Column(db.String(55), unique=True, index=True, 29 | nullable=False) 30 | client_type = db.Column(db.String(20), default='public') 31 | _redirect_uris = db.Column(db.Text) 32 | default_scope = db.Column(db.Text, default='email address') 33 | 34 | @property 35 | def user(self): 36 | return User.query.get(1) 37 | 38 | @property 39 | def redirect_uris(self): 40 | if self._redirect_uris: 41 | return self._redirect_uris.split() 42 | return [] 43 | 44 | @property 45 | def default_redirect_uri(self): 46 | return self.redirect_uris[0] 47 | 48 | @property 49 | def default_scopes(self): 50 | if self.default_scope: 51 | return self.default_scope.split() 52 | return [] 53 | 54 | @property 55 | def allowed_grant_types(self): 56 | return ['authorization_code', 'password', 'client_credentials', 57 | 'refresh_token'] 58 | 59 | 60 | class Grant(db.Model): 61 | id = db.Column(db.Integer, primary_key=True) 62 | user_id = db.Column( 63 | db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') 64 | ) 65 | user = relationship('User') 66 | 67 | client_id = db.Column( 68 | db.String(40), db.ForeignKey('client.client_id', ondelete='CASCADE'), 69 | nullable=False, 70 | ) 71 | client = relationship('Client') 72 | code = db.Column(db.String(255), index=True, nullable=False) 73 | 74 | redirect_uri = db.Column(db.String(255)) 75 | scope = db.Column(db.Text) 76 | expires = db.Column(db.DateTime) 77 | 78 | def delete(self): 79 | db.session.delete(self) 80 | db.session.commit() 81 | return self 82 | 83 | @property 84 | def scopes(self): 85 | if self.scope: 86 | return self.scope.split() 87 | return None 88 | 89 | 90 | class Token(db.Model): 91 | id = db.Column(db.Integer, primary_key=True) 92 | client_id = db.Column( 93 | db.String(40), db.ForeignKey('client.client_id', ondelete='CASCADE'), 94 | nullable=False, 95 | ) 96 | user_id = db.Column( 97 | db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') 98 | ) 99 | user = relationship('User') 100 | client = relationship('Client') 101 | token_type = db.Column(db.String(40)) 102 | access_token = db.Column(db.String(255)) 103 | refresh_token = db.Column(db.String(255)) 104 | expires = db.Column(db.DateTime) 105 | scope = db.Column(db.Text) 106 | 107 | def __init__(self, **kwargs): 108 | expires_in = kwargs.pop('expires_in') 109 | self.expires = datetime.utcnow() + timedelta(seconds=expires_in) 110 | for k, v in kwargs.items(): 111 | setattr(self, k, v) 112 | 113 | @property 114 | def scopes(self): 115 | if self.scope: 116 | return self.scope.split() 117 | return [] 118 | 119 | def delete(self): 120 | db.session.delete(self) 121 | db.session.commit() 122 | return self 123 | 124 | 125 | def current_user(): 126 | return g.user 127 | 128 | 129 | def cache_provider(app): 130 | oauth = OAuth2Provider(app) 131 | 132 | bind_sqlalchemy(oauth, db.session, user=User, 133 | token=Token, client=Client) 134 | 135 | app.config.update({'OAUTH2_CACHE_TYPE': 'simple'}) 136 | bind_cache_grant(app, oauth, current_user) 137 | return oauth 138 | 139 | 140 | def sqlalchemy_provider(app): 141 | oauth = OAuth2Provider(app) 142 | 143 | bind_sqlalchemy(oauth, db.session, user=User, token=Token, 144 | client=Client, grant=Grant, current_user=current_user) 145 | 146 | return oauth 147 | 148 | 149 | def default_provider(app): 150 | oauth = OAuth2Provider(app) 151 | 152 | @oauth.clientgetter 153 | def get_client(client_id): 154 | return Client.query.filter_by(client_id=client_id).first() 155 | 156 | @oauth.grantgetter 157 | def get_grant(client_id, code): 158 | return Grant.query.filter_by(client_id=client_id, code=code).first() 159 | 160 | @oauth.tokengetter 161 | def get_token(access_token=None, refresh_token=None): 162 | if access_token: 163 | return Token.query.filter_by(access_token=access_token).first() 164 | if refresh_token: 165 | return Token.query.filter_by(refresh_token=refresh_token).first() 166 | return None 167 | 168 | @oauth.grantsetter 169 | def set_grant(client_id, code, request, *args, **kwargs): 170 | expires = datetime.utcnow() + timedelta(seconds=100) 171 | grant = Grant( 172 | client_id=client_id, 173 | code=code['code'], 174 | redirect_uri=request.redirect_uri, 175 | scope=' '.join(request.scopes), 176 | user_id=g.user.id, 177 | expires=expires, 178 | ) 179 | db.session.add(grant) 180 | db.session.commit() 181 | 182 | @oauth.tokensetter 183 | def set_token(token, request, *args, **kwargs): 184 | # In real project, a token is unique bound to user and client. 185 | # Which means, you don't need to create a token every time. 186 | tok = Token(**token) 187 | tok.user_id = request.user.id 188 | tok.client_id = request.client.client_id 189 | db.session.add(tok) 190 | db.session.commit() 191 | 192 | @oauth.usergetter 193 | def get_user(username, password, *args, **kwargs): 194 | # This is optional, if you don't need password credential 195 | # there is no need to implement this method 196 | return User.query.filter_by(username=username).first() 197 | 198 | return oauth 199 | 200 | 201 | def prepare_app(app): 202 | db.init_app(app) 203 | db.app = app 204 | db.create_all() 205 | 206 | client1 = Client( 207 | name='dev', client_id='dev', client_secret='dev', 208 | _redirect_uris=( 209 | 'http://localhost:8000/authorized ' 210 | 'http://localhost/authorized' 211 | ), 212 | ) 213 | 214 | client2 = Client( 215 | name='confidential', client_id='confidential', 216 | client_secret='confidential', client_type='confidential', 217 | _redirect_uris=( 218 | 'http://localhost:8000/authorized ' 219 | 'http://localhost/authorized' 220 | ), 221 | ) 222 | 223 | user = User(username='admin') 224 | 225 | temp_grant = Grant( 226 | user_id=1, client_id='confidential', 227 | code='12345', scope='email', 228 | expires=datetime.utcnow() + timedelta(seconds=100) 229 | ) 230 | 231 | access_token = Token( 232 | user_id=1, client_id='dev', access_token='expired', expires_in=0 233 | ) 234 | 235 | try: 236 | db.session.add(client1) 237 | db.session.add(client2) 238 | db.session.add(user) 239 | db.session.add(temp_grant) 240 | db.session.add(access_token) 241 | db.session.commit() 242 | except: 243 | db.session.rollback() 244 | return app 245 | 246 | 247 | def create_server(app, oauth=None): 248 | if not oauth: 249 | oauth = default_provider(app) 250 | 251 | app = prepare_app(app) 252 | 253 | @app.before_request 254 | def load_current_user(): 255 | user = User.query.get(1) 256 | g.user = user 257 | 258 | @app.route('/home') 259 | def home(): 260 | return render_template('home.html') 261 | 262 | @app.route('/oauth/authorize', methods=['GET', 'POST']) 263 | @oauth.authorize_handler 264 | def authorize(*args, **kwargs): 265 | # NOTICE: for real project, you need to require login 266 | if request.method == 'GET': 267 | # render a page for user to confirm the authorization 268 | return render_template('confirm.html') 269 | 270 | if request.method == 'HEAD': 271 | # if HEAD is supported properly, request parameters like 272 | # client_id should be validated the same way as for 'GET' 273 | response = make_response('', 200) 274 | response.headers['X-Client-ID'] = kwargs.get('client_id') 275 | return response 276 | 277 | confirm = request.form.get('confirm', 'no') 278 | return confirm == 'yes' 279 | 280 | @app.route('/oauth/token', methods=['POST', 'GET']) 281 | @oauth.token_handler 282 | def access_token(): 283 | return {} 284 | 285 | @app.route('/oauth/revoke', methods=['POST']) 286 | @oauth.revoke_handler 287 | def revoke_token(): 288 | pass 289 | 290 | @app.route('/api/email') 291 | @oauth.require_oauth('email') 292 | def email_api(): 293 | oauth = request.oauth 294 | return jsonify(email='me@oauth.net', username=oauth.user.username) 295 | 296 | @app.route('/api/client') 297 | @oauth.require_oauth() 298 | def client_api(): 299 | oauth = request.oauth 300 | return jsonify(client=oauth.client.name) 301 | 302 | @app.route('/api/address/') 303 | @oauth.require_oauth('address') 304 | def address_api(city): 305 | oauth = request.oauth 306 | return jsonify(address=city, username=oauth.user.username) 307 | 308 | @app.route('/api/method', methods=['GET', 'POST', 'PUT', 'DELETE']) 309 | @oauth.require_oauth() 310 | def method_api(): 311 | return jsonify(method=request.method) 312 | 313 | @oauth.invalid_response 314 | def require_oauth_invalid(req): 315 | return jsonify(message=req.error_message), 401 316 | 317 | return app 318 | 319 | 320 | if __name__ == '__main__': 321 | from flask import Flask 322 | app = Flask(__name__) 323 | app.debug = True 324 | app.secret_key = 'development' 325 | app.config.update({ 326 | 'SQLALCHEMY_DATABASE_URI': 'sqlite:///test.sqlite' 327 | }) 328 | app = create_server(app) 329 | app.run() 330 | -------------------------------------------------------------------------------- /flask_oauthlib/contrib/oauth2.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | flask_oauthlib.contrib.oauth2 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | SQLAlchemy and Grant-Caching for OAuth2 provider. 7 | 8 | contributed by: Randy Topliffe 9 | """ 10 | 11 | import logging 12 | from datetime import datetime, timedelta 13 | from .cache import Cache 14 | 15 | 16 | __all__ = ('bind_cache_grant', 'bind_sqlalchemy') 17 | 18 | 19 | log = logging.getLogger('flask_oauthlib') 20 | 21 | 22 | class Grant(object): 23 | """Grant is only used by `GrantCacheBinding` to store the data 24 | returned from the cache system. 25 | 26 | :param cache: Werkzeug cache instance 27 | :param client_id: ID of the client 28 | :param code: A random string 29 | :param redirect_uri: A URI string 30 | :param scopes: A space delimited list of scopes 31 | :param user: the authorizatopm user 32 | """ 33 | 34 | def __init__(self, cache=None, client_id=None, code=None, 35 | redirect_uri=None, scopes=None, user=None): 36 | self._cache = cache 37 | self.client_id = client_id 38 | self.code = code 39 | self.redirect_uri = redirect_uri 40 | self.scopes = scopes 41 | self.user = user 42 | 43 | def delete(self): 44 | """Removes itself from the cache 45 | 46 | Note: This is required by the oauthlib 47 | """ 48 | log.debug( 49 | "Deleting grant %s for client %s" % (self.code, self.client_id) 50 | ) 51 | self._cache.delete(self.key) 52 | return None 53 | 54 | @property 55 | def key(self): 56 | """The string used as the key for the cache""" 57 | return '%s%s' % (self.code, self.client_id) 58 | 59 | def __getitem__(self, item): 60 | return getattr(self, item) 61 | 62 | def keys(self): 63 | return ['client_id', 'code', 'redirect_uri', 'scopes', 'user'] 64 | 65 | 66 | def bind_cache_grant(app, provider, current_user, config_prefix='OAUTH2'): 67 | """Configures an :class:`OAuth2Provider` instance to use various caching 68 | systems to get and set the grant token. This removes the need to 69 | register :func:`grantgetter` and :func:`grantsetter` yourself. 70 | 71 | :param app: Flask application instance 72 | :param provider: :class:`OAuth2Provider` instance 73 | :param current_user: function that returns an :class:`User` object 74 | :param config_prefix: prefix for config 75 | 76 | A usage example:: 77 | 78 | oauth = OAuth2Provider(app) 79 | app.config.update({'OAUTH2_CACHE_TYPE': 'redis'}) 80 | 81 | bind_cache_grant(app, oauth, current_user) 82 | 83 | You can define which cache system you would like to use by setting the 84 | following configuration option:: 85 | 86 | OAUTH2_CACHE_TYPE = 'null' // memcache, simple, redis, filesystem 87 | 88 | For more information on the supported cache systems please visit: 89 | `Cache `_ 90 | """ 91 | cache = Cache(app, config_prefix) 92 | 93 | @provider.grantsetter 94 | def create_grant(client_id, code, request, *args, **kwargs): 95 | """Sets the grant token with the configured cache system""" 96 | grant = Grant( 97 | cache, 98 | client_id=client_id, 99 | code=code['code'], 100 | redirect_uri=request.redirect_uri, 101 | scopes=request.scopes, 102 | user=current_user(), 103 | ) 104 | log.debug("Set Grant Token with key %s" % grant.key) 105 | cache.set(grant.key, dict(grant)) 106 | 107 | @provider.grantgetter 108 | def get(client_id, code): 109 | """Gets the grant token with the configured cache system""" 110 | grant = Grant(cache, client_id=client_id, code=code) 111 | ret = cache.get(grant.key) 112 | if not ret: 113 | log.debug("Grant Token not found with key %s" % grant.key) 114 | return None 115 | log.debug("Grant Token found with key %s" % grant.key) 116 | for k, v in ret.items(): 117 | setattr(grant, k, v) 118 | return grant 119 | 120 | 121 | def bind_sqlalchemy(provider, session, user=None, client=None, 122 | token=None, grant=None, current_user=None): 123 | """Configures the given :class:`OAuth2Provider` instance with the 124 | required getters and setters for persistence with SQLAlchemy. 125 | 126 | An example of using all models:: 127 | 128 | oauth = OAuth2Provider(app) 129 | 130 | bind_sqlalchemy(oauth, session, user=User, client=Client, 131 | token=Token, grant=Grant, current_user=current_user) 132 | 133 | You can omit any model if you wish to register the functions yourself. 134 | It is also possible to override the functions by registering them 135 | afterwards:: 136 | 137 | oauth = OAuth2Provider(app) 138 | 139 | bind_sqlalchemy(oauth, session, user=User, client=Client, token=Token) 140 | 141 | @oauth.grantgetter 142 | def get_grant(client_id, code): 143 | pass 144 | 145 | @oauth.grantsetter 146 | def set_grant(client_id, code, request, *args, **kwargs): 147 | pass 148 | 149 | # register tokensetter with oauth but keeping the tokengetter 150 | # registered by `SQLAlchemyBinding` 151 | # You would only do this for the token and grant since user and client 152 | # only have getters 153 | @oauth.tokensetter 154 | def set_token(token, request, *args, **kwargs): 155 | pass 156 | 157 | Note that current_user is only required if you're using SQLAlchemy 158 | for grant caching. If you're using another caching system with 159 | GrantCacheBinding instead, omit current_user. 160 | 161 | :param provider: :class:`OAuth2Provider` instance 162 | :param session: A :class:`Session` object 163 | :param user: :class:`User` model 164 | :param client: :class:`Client` model 165 | :param token: :class:`Token` model 166 | :param grant: :class:`Grant` model 167 | :param current_user: function that returns a :class:`User` object 168 | """ 169 | if user: 170 | user_binding = UserBinding(user, session) 171 | provider.usergetter(user_binding.get) 172 | 173 | if client: 174 | client_binding = ClientBinding(client, session) 175 | provider.clientgetter(client_binding.get) 176 | 177 | if token: 178 | token_binding = TokenBinding(token, session, current_user) 179 | provider.tokengetter(token_binding.get) 180 | provider.tokensetter(token_binding.set) 181 | 182 | if grant: 183 | if not current_user: 184 | raise ValueError(('`current_user` is required' 185 | 'for Grant Binding')) 186 | grant_binding = GrantBinding(grant, session, current_user) 187 | provider.grantgetter(grant_binding.get) 188 | provider.grantsetter(grant_binding.set) 189 | 190 | 191 | class BaseBinding(object): 192 | """Base Binding 193 | 194 | :param model: SQLAlchemy Model class 195 | :param session: A :class:`Session` object 196 | """ 197 | 198 | def __init__(self, model, session): 199 | self.session = session 200 | self.model = model 201 | 202 | @property 203 | def query(self): 204 | """Determines which method of getting the query object for use""" 205 | if hasattr(self.model, 'query'): 206 | return self.model.query 207 | else: 208 | return self.session.query(self.model) 209 | 210 | 211 | class UserBinding(BaseBinding): 212 | """Object use by SQLAlchemyBinding to register the user getter""" 213 | 214 | def get(self, username, password, *args, **kwargs): 215 | """Returns the User object 216 | 217 | Returns None if the user isn't found or the passwords don't match 218 | 219 | :param username: username of the user 220 | :param password: password of the user 221 | """ 222 | user = self.query.filter_by(username=username).first() 223 | if user and user.check_password(password): 224 | return user 225 | return None 226 | 227 | 228 | class ClientBinding(BaseBinding): 229 | """Object use by SQLAlchemyBinding to register the client getter""" 230 | 231 | def get(self, client_id): 232 | """Returns a Client object with the given client ID 233 | 234 | :param client_id: ID if the client 235 | """ 236 | return self.query.filter_by(client_id=client_id).first() 237 | 238 | 239 | class TokenBinding(BaseBinding): 240 | """Object use by SQLAlchemyBinding to register the token 241 | getter and setter 242 | """ 243 | def __init__(self, model, session, current_user=None): 244 | self.current_user = current_user 245 | super(TokenBinding, self).__init__(model, session) 246 | 247 | def get(self, access_token=None, refresh_token=None): 248 | """returns a Token object with the given access token or refresh token 249 | 250 | :param access_token: User's access token 251 | :param refresh_token: User's refresh token 252 | """ 253 | if access_token: 254 | return self.query.filter_by(access_token=access_token).first() 255 | elif refresh_token: 256 | return self.query.filter_by(refresh_token=refresh_token).first() 257 | return None 258 | 259 | def set(self, token, request, *args, **kwargs): 260 | """Creates a Token object and removes all expired tokens that belong 261 | to the user 262 | 263 | :param token: token object 264 | :param request: OAuthlib request object 265 | """ 266 | if hasattr(request, 'user') and request.user: 267 | user = request.user 268 | elif self.current_user: 269 | # for implicit token 270 | user = self.current_user() 271 | 272 | client = request.client 273 | 274 | tokens = self.query.filter_by( 275 | client_id=client.client_id, 276 | user_id=user.id).all() 277 | if tokens: 278 | for tk in tokens: 279 | self.session.delete(tk) 280 | self.session.commit() 281 | 282 | expires_in = token.get('expires_in') 283 | expires = datetime.utcnow() + timedelta(seconds=expires_in) 284 | 285 | tok = self.model(**token) 286 | tok.expires = expires 287 | tok.client_id = client.client_id 288 | tok.user_id = user.id 289 | 290 | self.session.add(tok) 291 | self.session.commit() 292 | return tok 293 | 294 | 295 | class GrantBinding(BaseBinding): 296 | """Object use by SQLAlchemyBinding to register the grant 297 | getter and setter 298 | """ 299 | 300 | def __init__(self, model, session, current_user): 301 | self.current_user = current_user 302 | super(GrantBinding, self).__init__(model, session) 303 | 304 | def set(self, client_id, code, request, *args, **kwargs): 305 | """Creates Grant object with the given params 306 | 307 | :param client_id: ID of the client 308 | :param code: 309 | :param request: OAuthlib request object 310 | """ 311 | expires = datetime.utcnow() + timedelta(seconds=100) 312 | grant = self.model( 313 | client_id=request.client.client_id, 314 | code=code['code'], 315 | redirect_uri=request.redirect_uri, 316 | scope=' '.join(request.scopes), 317 | user=self.current_user(), 318 | expires=expires 319 | ) 320 | self.session.add(grant) 321 | 322 | self.session.commit() 323 | 324 | def get(self, client_id, code): 325 | """Get the Grant object with the given client ID and code 326 | 327 | :param client_id: ID of the client 328 | :param code: 329 | """ 330 | return self.query.filter_by(client_id=client_id, code=code).first() 331 | -------------------------------------------------------------------------------- /flask_oauthlib/contrib/client/application.py: -------------------------------------------------------------------------------- 1 | """ 2 | flask_oauthlib.contrib.client 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | An experiment client with requests-oauthlib as backend. 6 | """ 7 | 8 | import os 9 | import contextlib 10 | import warnings 11 | try: 12 | from urllib.parse import urljoin 13 | except ImportError: 14 | from urlparse import urljoin 15 | 16 | from flask import current_app, redirect, request 17 | from requests_oauthlib import OAuth1Session, OAuth2Session 18 | from requests_oauthlib.oauth1_session import TokenMissing 19 | from oauthlib.oauth2.rfc6749.errors import MissingCodeError 20 | from werkzeug.utils import import_string 21 | 22 | from .descriptor import OAuthProperty, WebSessionData 23 | from .structure import OAuth1Response, OAuth2Response 24 | from .exceptions import AccessTokenNotFound 25 | from .signals import request_token_fetched 26 | 27 | 28 | __all__ = ['OAuth1Application', 'OAuth2Application'] 29 | 30 | 31 | class BaseApplication(object): 32 | """The base class of OAuth application. 33 | 34 | An application instance could be used in mupltiple context. It never stores 35 | any session-scope state in the ``__dict__`` of itself. 36 | 37 | :param name: the name of this application. 38 | :param clients: optional. a reference to the cached clients dictionary. 39 | """ 40 | 41 | session_class = None 42 | endpoint_url = OAuthProperty('endpoint_url', default='') 43 | 44 | def __init__(self, name, clients=None, **kwargs): 45 | # oauth property required 46 | self.name = name 47 | 48 | if clients: 49 | self.clients = clients 50 | 51 | # other descriptor assignable attributes 52 | for k, v in kwargs.items(): 53 | if not hasattr(self.__class__, k): 54 | raise TypeError('descriptor %r not found' % k) 55 | setattr(self, k, v) 56 | 57 | def __repr__(self): 58 | class_name = self.__class__.__name__ 59 | return '<%s:%s at %s>' % (class_name, self.name, hex(id(self))) 60 | 61 | def tokengetter(self, fn): 62 | self._tokengetter = fn 63 | return fn 64 | 65 | def obtain_token(self): 66 | """Obtains the access token by calling ``tokengetter`` which was 67 | defined by users. 68 | 69 | :returns: token or ``None``. 70 | """ 71 | tokengetter = getattr(self, '_tokengetter', None) 72 | if tokengetter is None: 73 | raise RuntimeError('%r missing tokengetter' % self) 74 | return tokengetter() 75 | 76 | @property 77 | def client(self): 78 | """The lazy-created OAuth session with the return value of 79 | :meth:`tokengetter`. 80 | 81 | :returns: The OAuth session instance or ``None`` while token missing. 82 | """ 83 | token = self.obtain_token() 84 | if token is None: 85 | raise AccessTokenNotFound 86 | return self._make_client_with_token(token) 87 | 88 | def _make_client_with_token(self, token): 89 | """Uses cached client or create new one with specific token.""" 90 | cached_clients = getattr(self, 'clients', None) 91 | hashed_token = _hash_token(self, token) 92 | 93 | if cached_clients and hashed_token in cached_clients: 94 | return cached_clients[hashed_token] 95 | 96 | client = self.make_client(token) # implemented in subclasses 97 | if cached_clients: 98 | cached_clients[hashed_token] = client 99 | 100 | return client 101 | 102 | def authorize(self, callback_uri, code=302): 103 | """Redirects to third-part URL and authorizes. 104 | 105 | :param callback_uri: the callback URI. if you generate it with the 106 | :func:`~flask.url_for`, don't forget to use the 107 | ``_external=True`` keyword argument. 108 | :param code: default is 302. the HTTP code for redirection. 109 | :returns: the redirection response. 110 | """ 111 | raise NotImplementedError 112 | 113 | def authorized_response(self): 114 | """Obtains access token from third-part API. 115 | 116 | :returns: the response with the type of :class:`OAuthResponse` dict, 117 | or ``None`` if the authorization has been denied. 118 | """ 119 | raise NotImplementedError 120 | 121 | def request(self, method, url, token=None, *args, **kwargs): 122 | if token is None: 123 | client = self.client 124 | else: 125 | client = self._make_client_with_token(token) 126 | url = urljoin(self.endpoint_url, url) 127 | return getattr(client, method)(url, *args, **kwargs) 128 | 129 | def head(self, *args, **kwargs): 130 | return self.request('head', *args, **kwargs) 131 | 132 | def get(self, *args, **kwargs): 133 | return self.request('get', *args, **kwargs) 134 | 135 | def post(self, *args, **kwargs): 136 | return self.request('post', *args, **kwargs) 137 | 138 | def put(self, *args, **kwargs): 139 | return self.request('put', *args, **kwargs) 140 | 141 | def delete(self, *args, **kwargs): 142 | return self.request('delete', *args, **kwargs) 143 | 144 | def patch(self, *args, **kwargs): 145 | return self.request('patch', *args, **kwargs) 146 | 147 | 148 | class OAuth1Application(BaseApplication): 149 | """The remote application for OAuth 1.0a.""" 150 | 151 | request_token_url = OAuthProperty('request_token_url') 152 | access_token_url = OAuthProperty('access_token_url') 153 | authorization_url = OAuthProperty('authorization_url') 154 | 155 | consumer_key = OAuthProperty('consumer_key') 156 | consumer_secret = OAuthProperty('consumer_secret') 157 | 158 | session_class = OAuth1Session 159 | 160 | def make_client(self, token): 161 | """Creates a client with specific access token pair. 162 | 163 | :param token: a tuple of access token pair ``(token, token_secret)`` 164 | or a dictionary of access token response. 165 | :returns: a :class:`requests_oauthlib.oauth1_session.OAuth1Session` 166 | object. 167 | """ 168 | if isinstance(token, dict): 169 | access_token = token['oauth_token'] 170 | access_token_secret = token['oauth_token_secret'] 171 | else: 172 | access_token, access_token_secret = token 173 | return self.make_oauth_session( 174 | resource_owner_key=access_token, 175 | resource_owner_secret=access_token_secret) 176 | 177 | def authorize(self, callback_uri, code=302): 178 | # TODO add support for oauth_callback=oob (out-of-band) here 179 | # http://tools.ietf.org/html/rfc5849#section-2.1 180 | oauth = self.make_oauth_session(callback_uri=callback_uri) 181 | 182 | # fetches request token 183 | token = oauth.fetch_request_token(self.request_token_url) 184 | request_token_fetched.send(self, response=OAuth1Response(token)) 185 | # TODO check oauth_callback_confirmed here 186 | # http://tools.ietf.org/html/rfc5849#section-2.1 187 | 188 | # redirects to third-part URL 189 | authorization_url = oauth.authorization_url(self.authorization_url) 190 | return redirect(authorization_url, code) 191 | 192 | def authorized_response(self): 193 | oauth = self.make_oauth_session() 194 | 195 | # obtains verifier 196 | try: 197 | oauth.parse_authorization_response(request.url) 198 | except TokenMissing: 199 | return # authorization denied 200 | 201 | # obtains access token 202 | token = oauth.fetch_access_token(self.access_token_url) 203 | return OAuth1Response(token) 204 | 205 | def make_oauth_session(self, **kwargs): 206 | oauth = self.session_class( 207 | self.consumer_key, client_secret=self.consumer_secret, **kwargs) 208 | return oauth 209 | 210 | 211 | class OAuth2Application(BaseApplication): 212 | """The remote application for OAuth 2.""" 213 | 214 | session_class = OAuth2Session 215 | 216 | access_token_url = OAuthProperty('access_token_url') 217 | authorization_url = OAuthProperty('authorization_url') 218 | refresh_token_url = OAuthProperty('refresh_token_url', default='') 219 | 220 | client_id = OAuthProperty('client_id') 221 | client_secret = OAuthProperty('client_secret') 222 | scope = OAuthProperty('scope', default=None) 223 | 224 | compliance_fixes = OAuthProperty('compliance_fixes', default=None) 225 | 226 | _session_state = WebSessionData('state') 227 | _session_redirect_url = WebSessionData('redir') 228 | 229 | def make_client(self, token): 230 | """Creates a client with specific access token dictionary. 231 | 232 | :param token: a dictionary of access token response. 233 | :returns: a :class:`requests_oauthlib.oauth2_session.OAuth2Session` 234 | object. 235 | """ 236 | return self.make_oauth_session(token=token) 237 | 238 | def tokensaver(self, fn): 239 | """A decorator to register a callback function for saving refreshed 240 | token while the old token has expired and the ``refresh_token_url`` has 241 | been specified. 242 | 243 | It is necessary for using the automatic refresh mechanism. 244 | 245 | :param fn: the callback function with ``token`` as its unique argument. 246 | """ 247 | self._tokensaver = fn 248 | return fn 249 | 250 | def authorize(self, callback_uri, code=302, **kwargs): 251 | oauth = self.make_oauth_session(redirect_uri=callback_uri) 252 | authorization_url, state = oauth.authorization_url( 253 | self.authorization_url, **kwargs) 254 | self._session_state = state 255 | self._session_redirect_url = callback_uri 256 | return redirect(authorization_url, code) 257 | 258 | def authorized_response(self): 259 | oauth = self.make_oauth_session( 260 | state=self._session_state, 261 | redirect_uri=self._session_redirect_url) 262 | del self._session_state 263 | del self._session_redirect_url 264 | 265 | with self.insecure_transport(): 266 | try: 267 | token = oauth.fetch_token( 268 | self.access_token_url, client_secret=self.client_secret, 269 | authorization_response=request.url) 270 | except MissingCodeError: 271 | return 272 | 273 | return OAuth2Response(token) 274 | 275 | def make_oauth_session(self, **kwargs): 276 | kwargs.setdefault('scope', self.scope) 277 | 278 | # configures automatic token refresh if possible 279 | if self.refresh_token_url: 280 | if not hasattr(self, '_tokensaver'): 281 | raise RuntimeError('missing tokensaver') 282 | kwargs.setdefault('auto_refresh_url', self.refresh_token_url) 283 | kwargs.setdefault('auto_refresh_kwargs', { 284 | 'client_id': self.client_id, 285 | 'client_secret': self.client_secret, 286 | }) 287 | kwargs.setdefault('token_updater', self._tokensaver) 288 | 289 | # creates session 290 | oauth = self.session_class(self.client_id, **kwargs) 291 | 292 | # patches session 293 | compliance_fixes = self.compliance_fixes 294 | if compliance_fixes is not None: 295 | if compliance_fixes.startswith('.'): 296 | compliance_fixes = \ 297 | 'requests_oauthlib.compliance_fixes' + compliance_fixes 298 | apply_fixes = import_string(compliance_fixes) 299 | oauth = apply_fixes(oauth) 300 | 301 | return oauth 302 | 303 | @contextlib.contextmanager 304 | def insecure_transport(self): 305 | """Creates a context to enable the oauthlib environment variable in 306 | order to debug with insecure transport. 307 | """ 308 | origin = os.environ.get('OAUTHLIB_INSECURE_TRANSPORT') 309 | if current_app.debug or current_app.testing: 310 | try: 311 | os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' 312 | yield 313 | finally: 314 | if origin: 315 | os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = origin 316 | else: 317 | os.environ.pop('OAUTHLIB_INSECURE_TRANSPORT', None) 318 | else: 319 | if origin: 320 | warnings.warn( 321 | 'OAUTHLIB_INSECURE_TRANSPORT has been found in os.environ ' 322 | 'but the app is not running in debug mode or testing mode.' 323 | ' It may put you in danger of the Man-in-the-middle attack' 324 | ' while using OAuth 2.', RuntimeWarning) 325 | yield 326 | 327 | 328 | def _hash_token(application, token): 329 | """Creates a hashable object for given token then we could use it as a 330 | dictionary key. 331 | """ 332 | if isinstance(token, dict): 333 | hashed_token = tuple(sorted(token.items())) 334 | elif isinstance(token, tuple): 335 | hashed_token = token 336 | else: 337 | raise TypeError('%r is unknown type of token' % token) 338 | 339 | return (application.__class__.__name__, application.name, hashed_token) 340 | -------------------------------------------------------------------------------- /tests/oauth2/test_oauth2.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import json 4 | import base64 5 | from flask import Flask 6 | from mock import MagicMock 7 | from .server import ( 8 | create_server, 9 | db, 10 | cache_provider, 11 | sqlalchemy_provider, 12 | default_provider, 13 | Token 14 | ) 15 | from .client import create_client 16 | from .._base import BaseSuite, clean_url 17 | from .._base import to_bytes as b 18 | from .._base import to_unicode as u 19 | 20 | 21 | class OAuthSuite(BaseSuite): 22 | @property 23 | def database(self): 24 | return db 25 | 26 | def create_oauth_provider(app): 27 | raise NotImplementedError('Each test class must' 28 | 'implement this method.') 29 | 30 | def create_app(self): 31 | app = Flask(__name__) 32 | app.debug = True 33 | app.testing = True 34 | app.secret_key = 'development' 35 | return app 36 | 37 | def setup_app(self, app): 38 | oauth = self.create_oauth_provider(app) 39 | create_server(app, oauth) 40 | client = create_client(app) 41 | client.http_request = MagicMock( 42 | side_effect=self.patch_request(app) 43 | ) 44 | self.oauth_client = client 45 | return app 46 | 47 | 48 | authorize_url = ( 49 | '/oauth/authorize?response_type=code&client_id=dev' 50 | '&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fauthorized&scope=email' 51 | ) 52 | 53 | 54 | def _base64(text): 55 | return u(base64.b64encode(b(text))) 56 | 57 | 58 | auth_code = _base64('confidential:confidential') 59 | 60 | 61 | class TestWebAuth(OAuthSuite): 62 | 63 | def create_oauth_provider(self, app): 64 | return default_provider(app) 65 | 66 | def test_login(self): 67 | rv = self.client.get('/login') 68 | assert 'response_type=code' in rv.location 69 | 70 | def test_oauth_authorize_invalid_url(self): 71 | rv = self.client.get('/oauth/authorize') 72 | assert 'invalid_client_id' in rv.location 73 | 74 | def test_oauth_authorize_valid_url(self): 75 | rv = self.client.get(authorize_url) 76 | assert b'' in rv.data 77 | 78 | rv = self.client.post(authorize_url, data=dict( 79 | confirm='no' 80 | )) 81 | assert 'access_denied' in rv.location 82 | 83 | rv = self.client.post(authorize_url, data=dict( 84 | confirm='yes' 85 | )) 86 | # success 87 | assert 'code=' in rv.location 88 | assert 'state' not in rv.location 89 | 90 | # test state 91 | rv = self.client.post(authorize_url + '&state=foo', data=dict( 92 | confirm='yes' 93 | )) 94 | assert 'code=' in rv.location 95 | assert 'state' in rv.location 96 | 97 | def test_http_head_oauth_authorize_valid_url(self): 98 | rv = self.client.head(authorize_url) 99 | assert rv.headers['X-Client-ID'] == 'dev' 100 | 101 | def test_get_access_token(self): 102 | rv = self.client.post(authorize_url, data={'confirm': 'yes'}) 103 | rv = self.client.get(clean_url(rv.location)) 104 | assert b'access_token' in rv.data 105 | 106 | def test_full_flow(self): 107 | rv = self.client.post(authorize_url, data={'confirm': 'yes'}) 108 | rv = self.client.get(clean_url(rv.location)) 109 | assert b'access_token' in rv.data 110 | 111 | rv = self.client.get('/') 112 | assert b'username' in rv.data 113 | 114 | rv = self.client.get('/address') 115 | assert rv.status_code == 401 116 | assert b'message' in rv.data 117 | 118 | rv = self.client.get('/method/post') 119 | assert b'POST' in rv.data 120 | 121 | rv = self.client.get('/method/put') 122 | assert b'PUT' in rv.data 123 | 124 | rv = self.client.get('/method/delete') 125 | assert b'DELETE' in rv.data 126 | 127 | def test_no_bear_token(self): 128 | @self.oauth_client.tokengetter 129 | def get_oauth_token(): 130 | return 'foo', '' 131 | 132 | rv = self.client.get('/method/put') 133 | assert b'token not found' in rv.data 134 | 135 | def test_expires_bear_token(self): 136 | @self.oauth_client.tokengetter 137 | def get_oauth_token(): 138 | return 'expired', '' 139 | 140 | rv = self.client.get('/method/put') 141 | assert b'token is expired' in rv.data 142 | 143 | def test_get_client(self): 144 | rv = self.client.post(authorize_url, data={'confirm': 'yes'}) 145 | rv = self.client.get(clean_url(rv.location)) 146 | rv = self.client.get("/client") 147 | assert b'dev' in rv.data 148 | 149 | def test_invalid_response_type(self): 150 | authorize_url = ( 151 | '/oauth/authorize?response_type=invalid&client_id=dev' 152 | '&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fauthorized' 153 | '&scope=email' 154 | ) 155 | rv = self.client.post(authorize_url, data={'confirm': 'yes'}) 156 | rv = self.client.get(clean_url(rv.location)) 157 | assert b'error' in rv.data 158 | 159 | def test_invalid_scope(self): 160 | authorize_url = ( 161 | '/oauth/authorize?response_type=code&client_id=dev' 162 | '&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fauthorized' 163 | '&scope=invalid' 164 | ) 165 | rv = self.client.get(authorize_url) 166 | rv = self.client.get(clean_url(rv.location)) 167 | assert b'error' in rv.data 168 | assert b'invalid_scope' in rv.data 169 | 170 | 171 | class TestWebAuthCached(TestWebAuth): 172 | 173 | def create_oauth_provider(self, app): 174 | return cache_provider(app) 175 | 176 | 177 | class TestWebAuthSQLAlchemy(TestWebAuth): 178 | 179 | def create_oauth_provider(self, app): 180 | return sqlalchemy_provider(app) 181 | 182 | 183 | class TestRefreshToken(OAuthSuite): 184 | 185 | def create_oauth_provider(self, app): 186 | return default_provider(app) 187 | 188 | def test_refresh_token_in_password_grant(self): 189 | url = ('/oauth/token?grant_type=password' 190 | '&scope=email+address&username=admin&password=admin') 191 | rv = self.client.get(url, headers={ 192 | 'Authorization': 'Basic %s' % auth_code, 193 | }) 194 | assert b'access_token' in rv.data 195 | data = json.loads(u(rv.data)) 196 | 197 | args = (data.get('scope').replace(' ', '+'), 198 | data.get('refresh_token')) 199 | url = ('/oauth/token?grant_type=refresh_token' 200 | '&scope=%s&refresh_token=%s') 201 | url = url % args 202 | rv = self.client.get(url, headers={ 203 | 'Authorization': 'Basic %s' % auth_code, 204 | }) 205 | assert b'access_token' in rv.data 206 | 207 | def test_refresh_token_in_authorization_code(self): 208 | rv = self.client.post(authorize_url, data={'confirm': 'yes'}) 209 | rv = self.client.get(clean_url(rv.location)) 210 | data = json.loads(u(rv.data)) 211 | 212 | args = (data.get('scope').replace(' ', '+'), 213 | data.get('refresh_token'), 'dev', 'dev') 214 | url = ('/oauth/token?grant_type=refresh_token' 215 | '&scope=%s&refresh_token=%s' 216 | '&client_id=%s&client_secret=%s') 217 | url = url % args 218 | rv = self.client.get(url) 219 | assert b'access_token' in rv.data 220 | 221 | 222 | class TestRefreshTokenCached(TestRefreshToken): 223 | 224 | def create_oauth_provider(self, app): 225 | return cache_provider(app) 226 | 227 | 228 | class TestRefreshTokenSQLAlchemy(TestRefreshToken): 229 | 230 | def create_oauth_provider(self, app): 231 | return sqlalchemy_provider(app) 232 | 233 | 234 | class TestRevokeToken(OAuthSuite): 235 | 236 | def create_oauth_provider(self, app): 237 | return default_provider(app) 238 | 239 | def get_token(self): 240 | url = ('/oauth/token?grant_type=password' 241 | '&scope=email+address&username=admin&password=admin') 242 | rv = self.client.get(url, headers={ 243 | 'Authorization': 'Basic %s' % auth_code, 244 | }) 245 | assert b'_token' in rv.data 246 | return json.loads(u(rv.data)) 247 | 248 | def test_revoke_token(self): 249 | data = self.get_token() 250 | tok = Token.query.filter_by( 251 | refresh_token=data['refresh_token']).first() 252 | assert tok.refresh_token == data['refresh_token'] 253 | 254 | revoke_url = '/oauth/revoke' 255 | args = {'token': data['refresh_token']} 256 | self.client.post(revoke_url, data=args, headers={ 257 | 'Authorization': 'Basic %s' % auth_code, 258 | }) 259 | 260 | tok = Token.query.filter_by( 261 | refresh_token=data['refresh_token']).first() 262 | assert tok is None 263 | 264 | def test_revoke_token_with_hint(self): 265 | data = self.get_token() 266 | tok = Token.query.filter_by( 267 | access_token=data['access_token']).first() 268 | assert tok.access_token == data['access_token'] 269 | 270 | revoke_url = '/oauth/revoke' 271 | args = {'token': data['access_token'], 272 | 'token_type_hint': 'access_token'} 273 | self.client.post(revoke_url, data=args, headers={ 274 | 'Authorization': 'Basic %s' % auth_code, 275 | }) 276 | 277 | tok = Token.query.filter_by( 278 | access_token=data['access_token']).first() 279 | assert tok is None 280 | 281 | 282 | class TestRevokeTokenCached(TestRefreshToken): 283 | 284 | def create_oauth_provider(self, app): 285 | return cache_provider(app) 286 | 287 | 288 | class TestRevokeTokenSQLAlchemy(TestRefreshToken): 289 | 290 | def create_oauth_provider(self, app): 291 | return sqlalchemy_provider(app) 292 | 293 | 294 | class TestCredentialAuth(OAuthSuite): 295 | 296 | def create_oauth_provider(self, app): 297 | return default_provider(app) 298 | 299 | def test_get_access_token(self): 300 | url = ('/oauth/token?grant_type=client_credentials' 301 | '&scope=email+address') 302 | rv = self.client.get(url, headers={ 303 | 'Authorization': 'Basic %s' % auth_code, 304 | }) 305 | assert b'access_token' in rv.data 306 | 307 | def test_invalid_auth_header(self): 308 | url = ('/oauth/token?grant_type=client_credentials' 309 | '&scope=email+address') 310 | rv = self.client.get(url, headers={ 311 | 'Authorization': 'Basic foobar' 312 | }) 313 | assert b'invalid_client' in rv.data 314 | 315 | def test_no_client(self): 316 | auth_code = _base64('none:confidential') 317 | url = ('/oauth/token?grant_type=client_credentials' 318 | '&scope=email+address') 319 | rv = self.client.get(url, headers={ 320 | 'Authorization': 'Basic %s' % auth_code, 321 | }) 322 | assert b'invalid_client' in rv.data 323 | 324 | def test_wrong_secret_client(self): 325 | auth_code = _base64('confidential:wrong') 326 | url = ('/oauth/token?grant_type=client_credentials' 327 | '&scope=email+address') 328 | rv = self.client.get(url, headers={ 329 | 'Authorization': 'Basic %s' % auth_code, 330 | }) 331 | assert b'invalid_client' in rv.data 332 | 333 | 334 | class TestCredentialAuthCached(TestCredentialAuth): 335 | 336 | def create_oauth_provider(self, app): 337 | return cache_provider(app) 338 | 339 | 340 | class TestCredentialAuthSQLAlchemy(TestCredentialAuth): 341 | 342 | def create_oauth_provider(self, app): 343 | return sqlalchemy_provider(app) 344 | 345 | 346 | class TestTokenGenerator(OAuthSuite): 347 | 348 | def create_oauth_provider(self, app): 349 | 350 | def generator(request): 351 | return 'foobar' 352 | 353 | app.config['OAUTH2_PROVIDER_TOKEN_GENERATOR'] = generator 354 | return default_provider(app) 355 | 356 | def test_get_access_token(self): 357 | rv = self.client.post(authorize_url, data={'confirm': 'yes'}) 358 | rv = self.client.get(clean_url(rv.location)) 359 | data = json.loads(u(rv.data)) 360 | assert data['access_token'] == 'foobar' 361 | assert data['refresh_token'] == 'foobar' 362 | 363 | 364 | class TestRefreshTokenGenerator(OAuthSuite): 365 | 366 | def create_oauth_provider(self, app): 367 | 368 | def at_generator(request): 369 | return 'foobar' 370 | 371 | def rt_generator(request): 372 | return 'abracadabra' 373 | 374 | app.config['OAUTH2_PROVIDER_TOKEN_GENERATOR'] = at_generator 375 | app.config['OAUTH2_PROVIDER_REFRESH_TOKEN_GENERATOR'] = rt_generator 376 | return default_provider(app) 377 | 378 | def test_get_access_token(self): 379 | rv = self.client.post(authorize_url, data={'confirm': 'yes'}) 380 | rv = self.client.get(clean_url(rv.location)) 381 | data = json.loads(u(rv.data)) 382 | assert data['access_token'] == 'foobar' 383 | assert data['refresh_token'] == 'abracadabra' 384 | 385 | 386 | class TestConfidentialClient(OAuthSuite): 387 | 388 | def create_oauth_provider(self, app): 389 | return default_provider(app) 390 | 391 | def test_get_access_token(self): 392 | url = ('/oauth/token?grant_type=authorization_code&code=12345' 393 | '&scope=email') 394 | rv = self.client.get(url, headers={ 395 | 'Authorization': 'Basic %s' % auth_code 396 | }) 397 | assert b'access_token' in rv.data 398 | 399 | def test_invalid_grant(self): 400 | url = ('/oauth/token?grant_type=authorization_code&code=54321' 401 | '&scope=email') 402 | rv = self.client.get(url, headers={ 403 | 'Authorization': 'Basic %s' % auth_code 404 | }) 405 | assert b'invalid_grant' in rv.data 406 | 407 | def test_invalid_client(self): 408 | url = ('/oauth/token?grant_type=authorization_code&code=12345' 409 | '&scope=email') 410 | rv = self.client.get(url, headers={ 411 | 'Authorization': 'Basic %s' % ('foo') 412 | }) 413 | assert b'invalid_client' in rv.data 414 | --------------------------------------------------------------------------------