├── .gitignore ├── .gitmodules ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── example ├── facebook.py ├── google.py ├── static │ ├── openid.png │ ├── sign-in.png │ └── style.css ├── templates │ ├── index.html │ └── layout.html └── tweet.py ├── flask_rauth.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.pyo 4 | docs/_build 5 | dist 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/_themes"] 2 | path = docs/_themes 3 | url = git://github.com/mitsuhiko/flask-sphinx-themes.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Flask-Rauth is a fork of Armin Ronacher's Flask-OAuth. 2 | Copyright (c) 2010 by Armin Ronacher. 3 | Copyright (c) 2012 by Joel Verhagen. 4 | 5 | Some rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are 9 | met: 10 | 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above 15 | copyright notice, this list of conditions and the following 16 | disclaimer in the documentation and/or other materials provided 17 | with the distribution. 18 | 19 | * The names of the contributors may not be used to endorse or 20 | promote products derived from this software without specific 21 | prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 29 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-Rauth 2 | =========== 3 | 4 | Adds OAuth 1.0/a, 2.0, and Ofly consumer support for `Flask`__, using the 5 | `rauth`__ library. 6 | 7 | __ http://flask.pocoo.org/ 8 | __ http://rauth.readthedocs.org/en/latest/ 9 | 10 | Flask-Rauth is a fork of Armin Ronacher's `Flask-OAuth`__. 11 | 12 | __ https://github.com/mitsuhiko/flask-oauth 13 | 14 | Documentation 15 | ~~~~~~~~~~~~~ 16 | 17 | Current documentation is available at the following URL: 18 | 19 | http://flask-rauth.readthedocs.org/en/latest/ 20 | 21 | Documentation hosting is provided by `Read the Docs 22 | `_. 23 | 24 | Examples 25 | ~~~~~~~~~~~~~~ 26 | 27 | Some people learn better by example, that's why I've provided the following 28 | examples for your edification: 29 | 30 | https://github.com/joelverhagen/flask-rauth/tree/master/example 31 | -------------------------------------------------------------------------------- /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-Rauth.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Rauth.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-Rauth" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Rauth" 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 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask-Rauth documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Aug 21 20:35:08 2012. 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 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | sys.path.append(os.path.join(os.path.dirname(__file__), "_themes")) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'Flask-Rauth' 45 | copyright = u'2012, Joel Verhagen' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '0.1' 53 | # The full version, including alpha/beta/rc tags. 54 | release = '0.1' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | #pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'flask_small' 95 | html_theme_options = { 96 | 'github_fork': 'joelverhagen/flask-rauth', 97 | 'index_logo': False 98 | } 99 | 100 | # Theme options are theme-specific and customize the look and feel of a theme 101 | # further. For a list of options available for each theme, see the 102 | # documentation. 103 | #html_theme_options = {} 104 | 105 | # Add any paths that contain custom themes here, relative to this directory. 106 | html_theme_path = ['_themes'] 107 | 108 | # The name for this set of Sphinx documents. If None, it defaults to 109 | # " v documentation". 110 | #html_title = None 111 | 112 | # A shorter title for the navigation bar. Default is the same as html_title. 113 | #html_short_title = None 114 | 115 | # The name of an image file (relative to this directory) to place at the top 116 | # of the sidebar. 117 | #html_logo = None 118 | 119 | # The name of an image file (within the static path) to use as favicon of the 120 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 121 | # pixels large. 122 | #html_favicon = None 123 | 124 | # Add any paths that contain custom static files (such as style sheets) here, 125 | # relative to this directory. They are copied after the builtin static files, 126 | # so a file named "default.css" will overwrite the builtin "default.css". 127 | #html_static_path = ['_static'] 128 | 129 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 130 | # using the given strftime format. 131 | #html_last_updated_fmt = '%b %d, %Y' 132 | 133 | # If true, SmartyPants will be used to convert quotes and dashes to 134 | # typographically correct entities. 135 | #html_use_smartypants = True 136 | 137 | # Custom sidebar templates, maps document names to template names. 138 | #html_sidebars = {} 139 | 140 | # Additional templates that should be rendered to pages, maps page names to 141 | # template names. 142 | #html_additional_pages = {} 143 | 144 | # If false, no module index is generated. 145 | #html_domain_indices = True 146 | 147 | # If false, no index is generated. 148 | #html_use_index = True 149 | 150 | # If true, the index is split into individual pages for each letter. 151 | #html_split_index = False 152 | 153 | # If true, links to the reST sources are added to the pages. 154 | #html_show_sourcelink = True 155 | 156 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 157 | #html_show_sphinx = True 158 | 159 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 160 | #html_show_copyright = True 161 | 162 | # If true, an OpenSearch description file will be output, and all pages will 163 | # contain a tag referring to it. The value of this option must be the 164 | # base URL from which the finished HTML is served. 165 | #html_use_opensearch = '' 166 | 167 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 168 | #html_file_suffix = None 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = 'Flask-Rauthdoc' 172 | 173 | 174 | # -- Options for LaTeX output -------------------------------------------------- 175 | 176 | latex_elements = { 177 | # The paper size ('letterpaper' or 'a4paper'). 178 | #'papersize': 'letterpaper', 179 | 180 | # The font size ('10pt', '11pt' or '12pt'). 181 | #'pointsize': '10pt', 182 | 183 | # Additional stuff for the LaTeX preamble. 184 | #'preamble': '', 185 | } 186 | 187 | # Grouping the document tree into LaTeX files. List of tuples 188 | # (source start file, target name, title, author, documentclass [howto/manual]). 189 | latex_documents = [ 190 | ('index', 'Flask-Rauth.tex', u'Flask-Rauth Documentation', 191 | u'Joel Verhagen', 'manual'), 192 | ] 193 | 194 | # The name of an image file (relative to this directory) to place at the top of 195 | # the title page. 196 | #latex_logo = None 197 | 198 | # For "manual" documents, if this is true, then toplevel headings are parts, 199 | # not chapters. 200 | #latex_use_parts = False 201 | 202 | # If true, show page references after internal links. 203 | #latex_show_pagerefs = False 204 | 205 | # If true, show URL addresses after external links. 206 | #latex_show_urls = False 207 | 208 | # Documents to append as an appendix to all manuals. 209 | #latex_appendices = [] 210 | 211 | # If false, no module index is generated. 212 | #latex_domain_indices = True 213 | 214 | 215 | # -- Options for manual page output -------------------------------------------- 216 | 217 | # One entry per manual page. List of tuples 218 | # (source start file, name, description, authors, manual section). 219 | man_pages = [ 220 | ('index', 'flask-rauth', u'Flask-Rauth Documentation', 221 | [u'Joel Verhagen'], 1) 222 | ] 223 | 224 | # If true, show URL addresses after external links. 225 | #man_show_urls = False 226 | 227 | 228 | # -- Options for Texinfo output ------------------------------------------------ 229 | 230 | # Grouping the document tree into Texinfo files. List of tuples 231 | # (source start file, target name, title, author, 232 | # dir menu entry, description, category) 233 | texinfo_documents = [ 234 | ('index', 'Flask-Rauth', u'Flask-Rauth Documentation', 235 | u'Joel Verhagen', 'Flask-Rauth', 'One line description of project.', 236 | 'Miscellaneous'), 237 | ] 238 | 239 | # Documents to append as an appendix to all manuals. 240 | #texinfo_appendices = [] 241 | 242 | # If false, no module index is generated. 243 | #texinfo_domain_indices = True 244 | 245 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 246 | #texinfo_show_urls = 'footnote' 247 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Flask-Rauth 2 | =========== 3 | 4 | .. currentmodule:: flask.ext.rauth 5 | 6 | Adds OAuth 1.0/a, 2.0, and Ofly consumer support for `Flask`__, using the 7 | `rauth`__ library. 8 | 9 | __ http://flask.pocoo.org/ 10 | __ http://rauth.readthedocs.org/en/latest/ 11 | 12 | Flask-Rauth is a fork of Armin Ronacher's `Flask-OAuth`__. 13 | 14 | __ https://github.com/mitsuhiko/flask-oauth 15 | 16 | .. contents:: 17 | :local: 18 | :backlinks: none 19 | 20 | Introduction 21 | ------------ 22 | 23 | Flask-Rauth is a Flask extensions that allows you to easily interact with OAuth 24 | 2.0, OAuth 1.0a, and Ofly enabled applications. Please note that Flask-Rauth is 25 | meant to only provide *consumer* support. This means that Flask-Rauth will 26 | allow users on your Flask website to sign in to external web services (i.e. the 27 | `Twitter API `_, `Facebook Graph API 28 | `_, `GitHub 29 | `_, etc). 30 | 31 | Once a user has authenticated with the external service, your server back-end 32 | execute calls on the external API on behalf of the user via a secure token 33 | process. This means that your application never has to deal with securing and 34 | transferring Twitter password, for example. *This is a good thing!* 35 | 36 | As mentioned before, Flask-Rauth supports the following protocols as a 37 | consumer: 38 | 39 | - OAuth 2.0 (`2.0 spec `_) 40 | - OAuth 1.0a (`1.0a spec `_) 41 | - Ofly (i.e. 42 | `Shutterfly `_) 43 | 44 | Tutorial 45 | -------- 46 | 47 | This tutorial should be able to help you get started with using OAuth 2.0 or 48 | OAuth 1.0a with your Flask application. 49 | 50 | Sign up with the external service 51 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 52 | 53 | **Note:** you can skip this section if you already have a consumer_key and 54 | consumer_secret. 55 | 56 | Many social networking websites have OAuth 1.0a or 2.0 capabilities. Find 57 | the developer documentation for an OAuth endpoint, and follow the documentation 58 | to get two important pieces of information: a **consumer key** and a **consumer 59 | secret**. You, as the web developer, are the consumer. The consumer key and 60 | consumer secret uniquely identify your website when you interact with the 61 | external web service. 62 | 63 | .. _consumer-label: 64 | 65 | Get a consumer key and secret 66 | ''''''''''''''''''''''''''''' 67 | 68 | To get the consumer key and consumer secret, you normally need to create an 69 | "app" entry using some developer interface. This normally includes providing 70 | a name, description, website and other fluffy information so that when your 71 | users authenticate into the external web service they see a message like 72 | "Joel's Awesome Flask App wants access to your Twitter information." 73 | 74 | For the lazy, here's a list of a few OAuth web services and their 75 | interfaces to get a consumer key and consumer secret. 76 | 77 | **Note:** you will need to log in with credentials for each respective web 78 | service for you to get a consumer key and consumer secret. 79 | 80 | - Facebook, `Apps`__ 81 | - GitHub, `Developer applications`__ 82 | - Google, `APIs Console`__ 83 | - LinkedIn, `List of Applications`__ 84 | - Twitter, `Apps`__ 85 | 86 | __ https://developers.facebook.com/apps 87 | __ https://github.com/settings/applications 88 | __ https://code.google.com/apis/console/ 89 | __ https://www.linkedin.com/secure/developer 90 | __ https://dev.twitter.com/apps 91 | 92 | .. _oauth2-note: 93 | 94 | OAuth 2.0 Note 95 | '''''''''''''' 96 | 97 | Most OAuth 2.0 web services not only require you to specify a name, 98 | description, etc. to get a consumer key and consumer secret, but they 99 | also require you to specify one or more static ``redirect_uri`` values. These 100 | values form a white list of URLs that a user can be redirected to after 101 | authentication. The value you set should match the :ref:`callback URL 102 | `. 103 | 104 | Determine the protocol 105 | ~~~~~~~~~~~~~~~~~~~~~~ 106 | 107 | Depending on the external web service you are using, you will need to use a 108 | different Flask-Rauth class. In the developer documentation, there will be some 109 | indication whether it uses OAuth 1.0a or OAuth 2.0. Use the table below to map 110 | the protocol to a Flask-Rauth service class. 111 | 112 | +------------+----------------+ 113 | | Protocol | Class | 114 | +============+================+ 115 | | OAuth 1.0a | `RauthOAuth1`_ | 116 | +------------+----------------+ 117 | | OAuth 2.0 | `RauthOAuth2`_ | 118 | +------------+----------------+ 119 | 120 | Enough with the talk, let's look at some code! 121 | 122 | Initialize the service object 123 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 124 | 125 | To get started, you will need to initialize a Flask-Rauth 126 | :ref:`service object `. 127 | 128 | .. _oauth2-init: 129 | 130 | OAuth 2.0 131 | ''''''''' 132 | 133 | Initialize a `RauthOAuth2`_ object. 134 | 135 | .. code-block:: python 136 | 137 | github = RauthOAuth2( 138 | name='github', 139 | base_url='https://api.github.com/', 140 | authorize_url='https://github.com/login/oauth/authorize', 141 | access_token_url='https://github.com/login/oauth/access_token' 142 | ) 143 | 144 | The `authorize_url` and `access_token_url` parameters are 145 | specific to the endpoint you are working with. 146 | 147 | See `Both Protocols`_ for information about the other keys. 148 | 149 | OAuth 1.0a 150 | '''''''''' 151 | 152 | Initialize a `RauthOAuth1`_ object: 153 | 154 | .. code-block:: python 155 | 156 | twitter = RauthOAuth1( 157 | name='twitter', 158 | base_url='https://api.twitter.com/1/', 159 | request_token_url='https://api.twitter.com/oauth/request_token', 160 | authorize_url='https://api.twitter.com/oauth/authorize', 161 | access_token_url='https://api.twitter.com/oauth/access_token' 162 | ) 163 | 164 | The `request_token_url`, `authorize_url`, and `access_token_url` 165 | parameters are specific to the endpoint you are working with. Notice the 166 | additional `request_token_url` parameter, compared to :ref:`OAuth 2.0 167 | `. 168 | 169 | See `Both Protocols`_ for information about the other keys. 170 | 171 | Both Protocols 172 | '''''''''''''' 173 | 174 | The `base_url` is **optional**, but can be provided so that 175 | :ref:`making requests ` is a bit easier. The 176 | `name` parameter is very important! This value will be used to 177 | determine the Flask configuration keys that contain the associated **consumer 178 | key** and **private key**. 179 | 180 | Set the consumer key and secret 181 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 182 | 183 | Assuming you've gotten a :ref:`consumer key and secret ` from 184 | the web service you are working with, you can provide them to your service 185 | object in a couple different ways. 186 | 187 | In your app config 188 | '''''''''''''''''' 189 | 190 | **This is the recommended method**, since it works very well when using an 191 | application factory to generate your Flask application object or when you may 192 | have multiple sets of consumer keys and secrets and you want to keep them all 193 | in one place. One use-case is if you would like a separate consumer key and 194 | secret for a development vs. testing vs. production environment. 195 | 196 | .. code-block:: python 197 | :emphasize-lines: 6-7, 9-10 198 | 199 | # create your application object 200 | # ... 201 | 202 | # set config values 203 | app.config.update( 204 | GITHUB_CONSUMER_KEY='', 205 | GITHUB_CONSUMER_SECRET='', 206 | 207 | TWITTER_CONSUMER_KEY='', 208 | TWITTER_CONSUMER_SECRET='', 209 | 210 | # other keys 211 | SECRET_KEY='just a secret key, to confound the bad guys', 212 | DEBUG=True 213 | # ... 214 | ) 215 | 216 | This setup should beg the following question: *how does Flask-Rauth know about 217 | these keys and secrets?* 218 | 219 | Well, you can register the service object that you initialized above 220 | as an extension with your app object, like this: 221 | 222 | .. code-block:: python 223 | 224 | # github is the RauthOAuth2 object, from above 225 | github.init_app(app) 226 | 227 | Or, you can simply let Flask-Rauth use Flask's super-useful ``current_app`` to 228 | get the currently active Flask application object, and look for the consumer 229 | key and secret in its configuration. 230 | 231 | Whether or not you call :func:`init_app`, the `name` parameter you pass to the 232 | service object's constructor is extremely important. When Flask-Rauth is 233 | looking for a consumer key or consumer secret, the name is upper cased (using 234 | ``name.upper()``) and appended with ``_CONSUMER_KEY`` and ``_CONSUMER_SECRET``, 235 | respectively. 236 | 237 | When initializing the service object 238 | '''''''''''''''''''''''''''''''''''' 239 | 240 | Alternatively, you can pass the consumer key and consumer secret when 241 | initializing your service object. 242 | 243 | .. code-block:: python 244 | :emphasize-lines: 6-7, 16-17 245 | 246 | github = RauthOAuth2( 247 | name='github', 248 | base_url='https://api.github.com/', 249 | authorize_url='https://github.com/login/oauth/authorize', 250 | access_token_url='https://github.com/login/oauth/access_token', 251 | consumer_key='', 252 | consumer_secret='' 253 | ) 254 | 255 | twitter = RauthOAuth1( 256 | name='twitter', 257 | base_url='https://api.twitter.com/1/', 258 | request_token_url='https://api.twitter.com/oauth/request_token', 259 | authorize_url='https://api.twitter.com/oauth/authorize' 260 | access_token_url='https://api.twitter.com/oauth/access_token', 261 | consumer_key='', 262 | consumer_secret='' 263 | ) 264 | 265 | This works just fine for applications that never need to worry about different 266 | keys for different running environments. However, :ref:`as mentioned above 267 | `, OAuth 2.0 requires you to predefine an absolute URL of where 268 | users can be redirected after authentication. If you have a test environment 269 | and production environment with different callback URLs (i.e. 270 | `http://test.example.com/github/authorized` and 271 | `http://www.example.com/github/authorized`), you may be forced to use a 272 | different consumer key and secret for each environment. 273 | 274 | .. _callback-label: 275 | 276 | Redirect the user 277 | ~~~~~~~~~~~~~~~~~ 278 | 279 | Now that you've initialized everything, it's time to hook the service object 280 | up. Both OAuth 2.0 and OAuth 1.0a have a step where the user is redirected from 281 | the consumer's website (your Flask web app) to the external web service (i.e. 282 | GitHub, Twitter, etc) for user authentication. Not only does the user log in on 283 | the external website, but they also choose whether your app is allowed to 284 | access their information. 285 | 286 | To kick off the authentication process, call the :func:`authorize` method on 287 | your service object, which will return a Flask `redirect` response. 288 | 289 | .. code-block:: python 290 | :emphasize-lines: 9 291 | 292 | # initialize the Flask application object 293 | # ... 294 | 295 | # initialize the GitHub OAuth 2.0 service 296 | # ... 297 | 298 | @app.route('/redirect') 299 | def redirect(): 300 | return github.authorize(callback=url_for('authorized', _external=True)) 301 | 302 | @app.route('/authorized') 303 | @github.authorized_handler() 304 | def authorized(...): 305 | # handle authorization 306 | 307 | .. _making-request-label: 308 | 309 | For both OAuth 1.0a and 2.0, the `callback` parameter is required. This tells 310 | OAuth server where to redirect the user after they have authenticated. If you 311 | use :func:`url_for` to generate the URL, make sure to generate an absolute URL 312 | using ``_external=True``. 313 | 314 | For OAuth 2.0, this `callback` parameter is mapped to the `redirect_uri` passed 315 | to the the external web service. 316 | 317 | Handle the authorization response 318 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 319 | 320 | So far, everything is (hopefully) pretty straightforward. When the user comes 321 | back from the external web service (after authorization), things get a bit more 322 | complex. 323 | 324 | In the previous step, you set the `callback` parameter for :func:`authorize` to 325 | be the absolute URL to a another route (in the example above, the route was 326 | ``/authorized``). This route will be hit by the user after authentication. 327 | 328 | This is a special route marked by the :func:`authorized_handler` decorator. 329 | This route will receive two parameters upon successful authorization: 330 | a special `RauthResponse` object and the token required for making requests on 331 | behalf of the authenticated user. If the first parameter is `None` or 332 | ``access_denied``, the authorization step failed (see `Handle if the user 333 | denies`_). 334 | 335 | When you declare your authorized handler, the top of it should look a lot like 336 | this: 337 | 338 | .. code-block:: python 339 | 340 | @app.route('/authorized') 341 | @github.authorized_handler() 342 | def authorized(response, access_token): 343 | # ... 344 | 345 | As you can see, you're expecting the two parameters that are mentioned above. 346 | 347 | Suppose your endpoint insists that a `GET` request must be issued when fetching 348 | the access token. Well, simply pass a `method` keyword argument to your 349 | :func:`authorized_handler` decorator. 350 | 351 | .. code-block:: python 352 | 353 | @app.route('/authorized') 354 | @acme.authorized_handler(method='GET') 355 | def authorized(response, access_token): 356 | # ... 357 | 358 | By default, a `POST` request is used. 359 | 360 | OAuth 2.0 361 | ''''''''' 362 | 363 | The second parameter will be an `access_token`. This is a single secret string 364 | used as a password, specific to your application, to make requests on behalf of 365 | your user. The string can have any length, so if you're storing it in a 366 | database, use a `Text` data type (unless you're very sure of the size, in which 367 | case you can get by with a CHAR/VARCHAR). 368 | 369 | If you're working with SQLAlchemy and declarative models (i.e. 370 | `Flask-SQLAlchemy`__), you're code might look a bit like this: 371 | 372 | __ http://packages.python.org/Flask-SQLAlchemy/ 373 | 374 | .. code-block:: python 375 | 376 | @app.route('/authorized') 377 | @github.authorized_handler() 378 | def authorized(resp, access_token): 379 | # save the access token to the database 380 | current_user.access_token = access_token 381 | db.session.commit() 382 | 383 | return redirect(url_for('index')) 384 | 385 | OAuth 1.0a 386 | '''''''''' 387 | 388 | The second parameter will be an `oauth_token`. This is different from OAuth 2.0 389 | because the token is actually a 2-tuple of an `oauth_token` and 390 | `oauth_token_secret`. Both of these are strings of any length and BOTH are used 391 | when making external web service calls on behalf of the user. 392 | 393 | If you're working with SQLAlchemy and declarative models (i.e. 394 | `Flask-SQLAlchemy`__), you're code might look a bit like this: 395 | 396 | __ http://packages.python.org/Flask-SQLAlchemy/ 397 | 398 | .. code-block:: python 399 | 400 | @app.route('/authorized') 401 | @linkedin.authorized_handler() 402 | def authorized(resp, oauth_token): 403 | # save the OAuth token to the database 404 | current_user.oauth_token = oauth_token[0] 405 | current_user.oauth_token_secret = oauth_token[1] 406 | db.session.commit() 407 | 408 | return redirect(url_for('index')) 409 | 410 | Handle if the user denies 411 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 412 | 413 | If you've worked with OAuth before, you'll know that there's the possibility 414 | that the user denies access to their information. 415 | 416 | OAuth 2.0 417 | ''''''''' 418 | 419 | This case is clearly defined in the OAuth 2.0 spec. The `redirect_uri` will 420 | have the query parameter ``error=access_denied`` added to it. 421 | 422 | With Flask-Rauth, all you need to do is check whether the first argument in 423 | your `authorized_handler` is equal to the string ``access_denied``. 424 | 425 | .. code-block:: python 426 | :emphasize-lines: 4-5 427 | 428 | @app.route('/authorized') 429 | @github.authorized_handler() 430 | def authorized(resp, access_token): 431 | if resp == 'access_denied': 432 | return 'You denied access, meanie.' 433 | 434 | flash('You have been logged in to GitHub successfully.') 435 | session['access_token'] = access_token 436 | 437 | return redirect(url_for('index')) 438 | 439 | OAuth 1.0a 440 | '''''''''' 441 | 442 | OAuth 1.0a, however, does not clearly define what the server should do if a 443 | user denies the consumer application's access to his or her information. 444 | Naturally, there is no common consensus in practice and many web APIs do it 445 | differently. 446 | 447 | Most OAuth 1.0a-enabled web services either do not have a `Deny` button at all 448 | (assuming the user will simply close the window or tab, thus cutting the OAuth 449 | process short) or have the `Deny` button redirect the user to the home page 450 | of the external web service. This makes life difficult for us consumers! 451 | 452 | Since the functionality isn't standard, you pretty much have to try it for each 453 | external web service that you want to work with. Whenever the first argument to 454 | your authorized handler is `None`, then we pretty much have to assume that the 455 | user denied access. 456 | 457 | For example, LinkedIn's `new authorization flow`__ does not indicate at all 458 | that the user denied access. They just redirect back to your `callback` 459 | without an `oauth_verifier` (which is a token that Flask-Rauth uses to fetch 460 | the OAuth token which can be used to make calls on behalf of the user). 461 | 462 | __ https://developer.linkedin.com/blog/making-it-easier-you-develop-linkedin 463 | 464 | .. code-block:: python 465 | :emphasize-lines: 5 466 | 467 | @app.route('/authorized') 468 | @linkedin.authorized_handler() 469 | def authorized(resp, oauth_token): 470 | if resp is None: 471 | return 'You denied access, meanie.' 472 | 473 | flash('You have been logged in to Twitter successfully.') 474 | session['oauth_token'] = oauth_token 475 | 476 | return redirect(url_for('index')) 477 | 478 | For every OAuth 1.0a endpoint that you hook up to, I *highly* recommend that 479 | you check the query parameters after you deny access to see if there is any 480 | explicit indication of the deny. 481 | 482 | In Twitter's case, if the user denies access to their Twitter account, then 483 | a "denied" query parameter will be tacked on the end of your callback. Thanks 484 | Twitter! 485 | 486 | .. code-block:: python 487 | :emphasize-lines: 5 488 | 489 | @app.route('/authorized') 490 | @twitter.authorized_handler() 491 | def authorized(resp, oauth_token): 492 | # check for the Twitter-specific "access_denied" indicator 493 | if resp is None and 'denied' in request.args: 494 | return 'You denied access, meanie.' 495 | elif resp is None: 496 | return 'Hey developer, something unexpected happened.' 497 | 498 | flash('You have been logged in to Twitter successfully.') 499 | session['oauth_token'] = oauth_token 500 | 501 | return redirect(url_for('index')) 502 | 503 | Make a request 504 | ~~~~~~~~~~~~~~ 505 | 506 | Now that you have a valid token, you can make requests on behalf of your user. 507 | All you need to do is call the :func:`get`, :func:`post`, :func:`put`, or 508 | :func:`delete` functions on your service object. The optional arguments for 509 | each request are outlined in the `Rauth documention`__. 510 | 511 | __ http://rauth.readthedocs.org/en/latest/#rauth.service.OAuth2Service.request 512 | 513 | Every API call requires that you provide the token that you aquired during user 514 | authorization. There are two ways to do this. 515 | 516 | Explicitly, by passing the token 517 | '''''''''''''''''''''''''''''''' 518 | 519 | You can pass the token as a keyword argument to one of the aforementioned 520 | request functions. When using OAuth 2.0, use the keyword `access_token`. 521 | 522 | .. code-block:: python 523 | 524 | # github is an OAuth 2.0 service object 525 | r = github.get('user', access_token=my_access_token) 526 | 527 | When using OAuth 1.0a, use the keyword `oauth_token`. 528 | 529 | .. code-block:: python 530 | 531 | # twitter is an OAuth 1.0a service object 532 | r = twitter.get('account/verify_credentials.json', oauth_token=token) 533 | 534 | This method of passing a token is most useful when you have to use multiple 535 | tokens at the same time (i.e. you are fetching repository information for more 536 | than one authorized GitHub user in a single request). 537 | 538 | Implicitly, by defining a token getter function 539 | ''''''''''''''''''''''''''''''''''''''''''''''' 540 | 541 | If you would like to tightly associate a specific token source (i.e. database, 542 | session, cookies) with each user, declaring a token getter is probably the 543 | cleanest solution. 544 | 545 | When using OAuth 2.0, return the `access_token` recieved after authorization. 546 | 547 | .. code-block:: python 548 | 549 | # github is an OAuth 2.0 service object 550 | @github.tokengetter 551 | def get_github_token(): 552 | return session.get('access_token') 553 | 554 | When using OAuth 1.0a, return the 2-tuple `oauth_token` recieved after 555 | authorization. 556 | 557 | .. code-block:: python 558 | 559 | # twitter is an OAuth 1.0a service object 560 | @twitter.tokengetter 561 | def get_twitter_token(): 562 | # g is Flask's global object 563 | user = g.user 564 | if user is not None: 565 | return user.oauth_token 566 | 567 | If no access token is available, just return `None`. 568 | 569 | After the request completes, a :ref:`RauthResponse ` object is 570 | returned. 571 | 572 | Examples 573 | -------- 574 | 575 | Make sure to check out the `example` directory if you're still confused about 576 | the API. 577 | 578 | `Examples`__, easily viewable in the GitHub source browser. 579 | 580 | __ https://github.com/joelverhagen/flask-rauth/tree/master/example 581 | 582 | API Reference 583 | ------------- 584 | 585 | .. module:: flask_rauth 586 | 587 | .. _services-label: 588 | 589 | Services 590 | ~~~~~~~~ 591 | 592 | RauthOAuth2 593 | ''''''''''' 594 | .. autoclass:: RauthOAuth2 595 | :members: 596 | 597 | RauthOAuth1 598 | ''''''''''' 599 | .. autoclass:: RauthOAuth1 600 | :members: 601 | 602 | RauthOfly 603 | ''''''''' 604 | .. autoclass:: RauthOfly 605 | :members: 606 | 607 | Helpers 608 | ~~~~~~~~~~~~~~ 609 | 610 | .. _response-label: 611 | 612 | .. autoclass:: RauthResponse 613 | :members: 614 | 615 | Internals 616 | ~~~~~~~~~ 617 | 618 | .. autoclass:: RauthServiceMixin 619 | :members: 620 | :exclude-members: consumer_secret_setter, consumer_key_setter 621 | 622 | .. autoexception:: RauthException 623 | 624 | .. autofunction:: get_etree 625 | 626 | .. autofunction:: parse_response 627 | 628 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 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. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-Rauth.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-Rauth.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /example/facebook.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session, request, Response 2 | from flask.ext.rauth import RauthOAuth2 3 | 4 | app = Flask(__name__) 5 | app.config.update( 6 | SECRET_KEY='just a secret key, to confound the bad guys', 7 | DEBUG=True 8 | ) 9 | 10 | # you can specify your consumer key and consumer secret when constructing 11 | # the Rauth service, like this: 12 | facebook = RauthOAuth2( 13 | name='facebook', 14 | base_url='https://graph.facebook.com/', 15 | access_token_url='https://graph.facebook.com/oauth/access_token', 16 | authorize_url='https://www.facebook.com/dialog/oauth', 17 | consumer_key='your_consumer_key', 18 | consumer_secret='your_consumer_secret' 19 | ) 20 | 21 | 22 | @app.route('/') 23 | def index(): 24 | return redirect(url_for('login')) 25 | 26 | 27 | @app.route('/login') 28 | def login(): 29 | return facebook.authorize(callback=url_for('authorized', 30 | next=request.args.get('next') or request.referrer or None, 31 | _external=True)) 32 | 33 | 34 | @app.route('/login/authorized') 35 | @facebook.authorized_handler() 36 | def authorized(resp, access_token): 37 | if resp == 'access_denied': 38 | return 'You denied access, meanie. Click here to try again.' % (url_for('login'),) 39 | 40 | session['access_token'] = access_token 41 | 42 | me = facebook.get('me') 43 | 44 | from pprint import pformat 45 | return Response(pformat(me.content), mimetype='text/plain') 46 | 47 | 48 | @facebook.tokengetter 49 | def get_facebook_oauth_token(): 50 | return session.get('access_token') 51 | 52 | 53 | if __name__ == '__main__': 54 | app.run() 55 | -------------------------------------------------------------------------------- /example/google.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session, Response 2 | from flask.ext.rauth import RauthOAuth2 3 | 4 | app = Flask(__name__) 5 | # you can specify the consumer key and consumer secret in the application, 6 | # like this: 7 | app.config.update( 8 | GOOGLE_CONSUMER_KEY='your_conumser_key', 9 | GOOGLE_CONSUMER_SECRET='your_conumser_secret', 10 | SECRET_KEY='just a secret key, to confound the bad guys', 11 | DEBUG=True 12 | ) 13 | 14 | google = RauthOAuth2( 15 | name='google', 16 | base_url='https://www.googleapis.com/oauth2/v1/', 17 | access_token_url='https://accounts.google.com/o/oauth2/token', 18 | authorize_url='https://accounts.google.com/o/oauth2/auth' 19 | ) 20 | 21 | # the Rauth service detects the consumer_key and consumer_secret using 22 | # `current_app`. 23 | 24 | @app.route('/') 25 | def index(): 26 | access_token = session.get('access_token') 27 | if access_token is None: 28 | return redirect(url_for('login')) 29 | 30 | userinfo = google.get('userinfo', access_token=access_token) 31 | 32 | from pprint import pformat 33 | return Response(pformat(userinfo.content), mimetype='text/plain') 34 | 35 | 36 | @app.route('/login') 37 | def login(): 38 | return google.authorize( 39 | callback=url_for('authorized', _external=True), 40 | scope='https://www.googleapis.com/auth/userinfo.profile') 41 | 42 | 43 | @app.route('/authorized') 44 | @google.authorized_handler() 45 | def authorized(resp, access_token): 46 | if resp == 'access_denied': 47 | return 'You denied access, meanie. Click here to try again.' % (url_for('login'),) 48 | 49 | session['access_token'] = access_token 50 | 51 | return redirect(url_for('index')) 52 | 53 | 54 | if __name__ == '__main__': 55 | app.run() 56 | -------------------------------------------------------------------------------- /example/static/openid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelverhagen/flask-rauth/50e1af457b81bd7122be19ca6a9edf8940e096b5/example/static/openid.png -------------------------------------------------------------------------------- /example/static/sign-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelverhagen/flask-rauth/50e1af457b81bd7122be19ca6a9edf8940e096b5/example/static/sign-in.png -------------------------------------------------------------------------------- /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.name }}! Wanna tweet something? 7 |

8 |

9 | 10 | 11 |

12 | {% if tweets %} 13 |

Your Timeline

14 | 20 | {% endif %} 21 | {% else %} 22 |

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

26 | sign in 28 | {% endif %} 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /example/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | {% block title %}Welcome{% endblock %} | Flask OAuth Example 3 | 5 |

Flask-Rauth Example

6 | 14 | {% for message in get_flashed_messages() %} 15 |

{{ message }} 16 | {% endfor %} 17 | {% block body %}{% endblock %} 18 | -------------------------------------------------------------------------------- /example/tweet.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Instructions: 3 | 4 | 1. Make sure you have Flask, Flask-Rauth, and SQLAlchemy installed. 5 | 6 | $ pip install Flask Flask-Rauth SQLAlchemy 7 | 8 | 2. Open a Python shell in this directory and execute the following: 9 | 10 | $ python 11 | >>> from tweet import init_db 12 | >>> init_db() 13 | >>> exit() 14 | 15 | This will initialize the SQLite database. 16 | 17 | 3. Start the application. 18 | 19 | $ python tweet.py 20 | 21 | 4. Navigate your web browser to where this app is being served (localhost, 22 | by default). 23 | ''' 24 | from flask import Flask, request, redirect, url_for, session, flash, g, render_template 25 | from flask.ext.rauth import RauthOAuth1 26 | 27 | from sqlalchemy import create_engine, Column, Integer, String, Text 28 | from sqlalchemy.orm import scoped_session, sessionmaker 29 | from sqlalchemy.ext.declarative import declarative_base 30 | 31 | # setup flask 32 | app = Flask(__name__) 33 | # you can specify the consumer key and consumer secret in the application, 34 | # like this: 35 | app.config.update( 36 | TWITTER_CONSUMER_KEY='your_consumer_key', 37 | TWITTER_CONSUMER_SECRET='your_consumer_secret', 38 | SECRET_KEY='just a secret key, to confound the bad guys', 39 | DEBUG = True 40 | ) 41 | 42 | 43 | # setup the twitter endpoint 44 | twitter = RauthOAuth1( 45 | name='twitter', 46 | base_url='https://api.twitter.com/1/', 47 | request_token_url='https://api.twitter.com/oauth/request_token', 48 | access_token_url='https://api.twitter.com/oauth/access_token', 49 | authorize_url='https://api.twitter.com/oauth/authorize' 50 | ) 51 | 52 | # this call simply initializes default an empty consumer key and secret in the app 53 | # config if none exist. 54 | # I've included it to match the "look" of Flask extensions 55 | twitter.init_app(app) 56 | 57 | # setup sqlalchemy 58 | engine = create_engine('sqlite:////tmp/tweet.db') 59 | db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) 60 | Base = declarative_base() 61 | Base.query = db_session.query_property() 62 | 63 | 64 | def init_db(): 65 | Base.metadata.create_all(bind=engine) 66 | 67 | 68 | class User(Base): 69 | __tablename__ = 'users' 70 | id = Column('user_id', Integer, primary_key=True) 71 | name = Column(String(60)) 72 | oauth_token = Column(Text) 73 | oauth_secret = Column(Text) 74 | 75 | def __init__(self, name): 76 | self.name = name 77 | 78 | 79 | @app.before_request 80 | def before_request(): 81 | g.user = None 82 | if 'user_id' in session: 83 | g.user = User.query.get(session['user_id']) 84 | 85 | 86 | @app.after_request 87 | def after_request(response): 88 | db_session.remove() 89 | return response 90 | 91 | 92 | @twitter.tokengetter 93 | def get_twitter_token(): 94 | ''' 95 | This is used by the API to look for the auth token and secret that are used 96 | for Twitter API calls. If you don't want to store this in the database, 97 | consider putting it into the session instead. 98 | 99 | Since the Twitter API is OAuth 1.0a, the `tokengetter` must return a 100 | 2-tuple: (oauth_token, oauth_secret). 101 | ''' 102 | user = g.user 103 | if user is not None: 104 | return user.oauth_token, user.oauth_secret 105 | 106 | 107 | @app.route('/') 108 | def index(): 109 | tweets = None 110 | if g.user is not None: 111 | resp = twitter.get('statuses/home_timeline.json') 112 | if resp.status == 200: 113 | tweets = resp.content 114 | else: 115 | flash('Unable to load tweets from Twitter. Maybe out of ' 116 | 'API calls or Twitter is overloaded.') 117 | return render_template('index.html', tweets=tweets) 118 | 119 | 120 | @app.route('/tweet', methods=['POST']) 121 | def tweet(): 122 | ''' 123 | Calls the remote twitter API to create a new status update. 124 | ''' 125 | if g.user is None: 126 | return redirect(url_for('login', next=request.url)) 127 | status = request.form['tweet'] 128 | if not status: 129 | return redirect(url_for('index')) 130 | resp = twitter.post('statuses/update.json', data={ 131 | 'status': status 132 | }) 133 | if resp.status == 403: 134 | flash('Your tweet was too long.') 135 | elif resp.status == 401: 136 | flash('Authorization error with Twitter.') 137 | else: 138 | flash('Successfully tweeted your tweet (ID: #%s)' % resp.content['id']) 139 | return redirect(url_for('index')) 140 | 141 | 142 | @app.route('/login') 143 | def login(): 144 | ''' 145 | Calling into `authorize` will cause the OAuth 1.0a machinery to kick 146 | in. If all has worked out as expected or if the user denied access to 147 | his/her information, the remote application will redirect back to the callback URL 148 | provided. 149 | 150 | Int our case, the 'authorized/' route handles the interaction after the redirect. 151 | ''' 152 | return twitter.authorize(callback=url_for('authorized', 153 | _external=True, 154 | next=request.args.get('next') or request.referrer or None)) 155 | 156 | 157 | @app.route('/logout') 158 | def logout(): 159 | session.pop('user_id', None) 160 | flash('You were signed out') 161 | return redirect(request.referrer or url_for('index')) 162 | 163 | 164 | @app.route('/authorized') 165 | @twitter.authorized_handler() 166 | def authorized(resp, oauth_token): 167 | ''' 168 | Called after authorization. After this function finished handling, 169 | the tokengetter from above is used to retrieve the 2-tuple containing the 170 | oauth_token and oauth_token_secret. 171 | 172 | Because reauthorization often changes any previous 173 | oauth_token/oauth_token_secret values, then we must update them in the 174 | database. 175 | 176 | If the application redirected back after denying, the `resp` passed 177 | to the function will be `None`. Unfortunately, OAuth 1.0a (the version 178 | that Twitter, LinkedIn, etc use) does not specify exactly what should 179 | happen when the user denies access. In the case of Twitter, a query 180 | parameter `denied=(some hash)` is appended to the redirect URL. 181 | ''' 182 | next_url = request.args.get('next') or url_for('index') 183 | 184 | # check for the Twitter-specific "access_denied" indicator 185 | if resp is None and 'denied' in request.args: 186 | flash(u'You denied the request to sign in.') 187 | return redirect(next_url) 188 | 189 | # pull out the nicely parsed response content. 190 | content = resp.content 191 | 192 | user = User.query.filter_by(name=content['screen_name']).first() 193 | 194 | # this if the first time signing in for this user 195 | if user is None: 196 | user = User(content['screen_name']) 197 | db_session.add(user) 198 | 199 | # we now update the oauth_token and oauth_token_secret 200 | # this involves destructuring the 2-tuple that is passed back from the 201 | # Twitter API, so it can be easily stored in the SQL database 202 | user.oauth_token = oauth_token[0] 203 | user.oauth_secret = oauth_token[1] 204 | db_session.commit() 205 | 206 | session['user_id'] = user.id 207 | flash('You were signed in') 208 | return redirect(next_url) 209 | 210 | 211 | if __name__ == '__main__': 212 | app.run() 213 | -------------------------------------------------------------------------------- /flask_rauth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | flask.ext.rauth 4 | ~~~~~~~~~~~~~~~ 5 | 6 | Adds OAuth 1.0/a, 2.0, and Ofly consumer support for Flask. 7 | 8 | Flask-Rauth is a fork of Armin Ronacher's Flask-OAuth. 9 | :copyright: (c) 2010 by Armin Ronacher. 10 | :copyright: (c) 2012 by Joel Verhagen. 11 | :license: BSD, see LICENSE for more details. 12 | ''' 13 | from functools import wraps 14 | from urlparse import urljoin 15 | from flask import request, session, redirect, current_app 16 | from werkzeug import parse_options_header 17 | from rauth.service import OAuth2Service, OAuth1Service, OflyService, Response, parse_utf8_qsl 18 | 19 | # specified by the OAuth 2.0 spec 20 | # http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1.4 21 | ACCESS_DENIED = 'access_denied' 22 | 23 | _etree = None 24 | def get_etree(): 25 | ''' 26 | Returns an elementtree implementation. Searches for `lxml.etree`, then 27 | `xml.etree.cElementTree`, then `xml.etree.ElementTree`. 28 | ''' 29 | global _etree 30 | if _etree is None: 31 | try: 32 | from lxml import etree 33 | _etree = etree 34 | except ImportError: 35 | try: 36 | from xml.etree import cElementTree 37 | _etree = cElementTree 38 | except ImportError: 39 | try: 40 | from xml.etree import ElementTree 41 | _etree = ElementTree 42 | except ImportError: 43 | pass 44 | return _etree 45 | 46 | def parse_response(resp): 47 | ''' 48 | Inspects a :class:`requests.Response` object and returns the content in a 49 | more usable form. The following parsing checks are done: 50 | 51 | 1. JSON, using the `json` attribute. 52 | 2. XML, using :func:`get_etree`. 53 | 3. RSS or Atom, using :mod:`feedparser`, if available. 54 | 4. Query string, using :func:`parse_utf8_qsl`. 55 | 5. If all else fails the plain-text `content` is returned. 56 | 57 | :param resp: A `requests.Response` object. 58 | ''' 59 | if resp.json is not None: 60 | return resp.json 61 | 62 | ct, _ = parse_options_header(resp.headers.get('content-type')) 63 | 64 | if ct in ('application/xml', 'text/xml'): 65 | etree = get_etree() 66 | if etree is not None: 67 | return etree.fromstring(resp.content) 68 | 69 | if ct in ('application/atom+xml', 'application/rss+xml'): 70 | try: 71 | import feedparser 72 | return feedparser.parse(resp.content) 73 | except: 74 | pass 75 | 76 | if isinstance(resp.content, basestring): 77 | return parse_utf8_qsl(resp.content) 78 | 79 | return resp.content 80 | 81 | class RauthException(RuntimeError): 82 | ''' 83 | Raised if authorization fails for some reason. 84 | 85 | :param message: A helpful error message for debugging. 86 | :param response: The :class:`requests.Response` object associated with the 87 | failure, if available. 88 | ''' 89 | message = None 90 | 91 | def __init__(self, message, response=None): 92 | # a helpful error message for debugging 93 | self.message = message 94 | 95 | # if available, the associate response object 96 | self.response = response 97 | 98 | def __str__(self): 99 | return self.message.encode('utf-8') 100 | 101 | def __unicode__(self): 102 | return self.message 103 | 104 | class RauthResponse(Response): 105 | ''' 106 | This class inherits :class:`rauth.service.Response`. 107 | 108 | :param resp: A :class:`rauth.service.Response`, whose `response` attribute 109 | will be re-wrapped, with better content parsing. 110 | ''' 111 | def __init__(self, resp): 112 | # the original response 113 | self.response = resp.response 114 | 115 | self._cached_content = None 116 | 117 | @property 118 | def content(self): 119 | ''' 120 | The content associated with the response. The content is parsed into a 121 | more useful format, if possible, using :func:`parse_response`. 122 | 123 | The content is cached, so that :func:`parse_response` is only run once. 124 | ''' 125 | if self._cached_content is None: 126 | # the parsed content from the server 127 | self._cached_content = parse_response(self.response) 128 | return self._cached_content 129 | 130 | @property 131 | def status(self): 132 | ''' 133 | The status code of the response. 134 | ''' 135 | return self.response.status_code 136 | 137 | @property 138 | def content_type(self): 139 | ''' 140 | The Content-Type of the response. 141 | ''' 142 | return self.response.headers.get('content-type') 143 | 144 | class RauthServiceMixin(object): 145 | ''' 146 | A mixin used to help glue Flask and `rauth` together. **You should not 147 | initialize this class on your own.** Instead, it will be initialized by one 148 | of the service objects above. 149 | 150 | :param app: An Flask application object to tie this extension to. 151 | :param base_url: A base URL value which, if provided, will be joined 152 | with the URL passed to requests made on this object. 153 | ''' 154 | def __init__(self, app, base_url): 155 | self.app = app 156 | if app is not None: 157 | self.init_app(app) 158 | 159 | self.base_url = base_url 160 | self.tokengetter_f = None 161 | 162 | def init_app(self, app): 163 | ''' 164 | Initializes the application with this object as an extension. 165 | 166 | This simply ensures that there are `config` entries for keys generated 167 | by :func:`_consumer_key_config` and :func:`_consumer_secret_config`, 168 | i.e. ``(NAME)_CONSUMER_KEY`` and ``(NAME)_CONSUMER_SECRET``. 169 | 170 | :param app: A Flask application object. 171 | ''' 172 | # the name attribute will be set by a rauth service 173 | app.config.setdefault(self._consumer_key_config()) 174 | app.config.setdefault(self._consumer_secret_config()) 175 | 176 | def tokengetter(self, f): 177 | ''' 178 | The tokengetter decorator used to provide a function that will return 179 | the required token before making a request. 180 | ''' 181 | self.tokengetter_f = f 182 | return f 183 | 184 | def _expand_url(self, url): 185 | # prepend the base base_url, if we have it 186 | if self.base_url is not None: 187 | url = urljoin(self.base_url, url) 188 | return url 189 | 190 | def _session_key(self, suffix): 191 | return '%s_%s_%s' % (self.name, self.__class__.__name__, suffix) 192 | 193 | @property 194 | def consumer_key(self): 195 | ''' 196 | Returns the consumer_key for this object. The following method is used 197 | to determine what the consumer_key is: 198 | 199 | 1. A `static_consumer_key`, set by passing a `consumer_key` to the 200 | constructor. 201 | 2. The `consumer_key` set in the config of an app passed to the 202 | constructor. The application config key is based on the name 203 | passed to the constructor. See :func:`init_app` for more 204 | information. 205 | 3. The `consumer_key` set in the config of the Flask `current_app`. 206 | ''' 207 | if self.static_consumer_key is not None: 208 | # if a consumer key was provided in the constructor, default to that 209 | return self.static_consumer_key 210 | elif self.app is not None and self._consumer_key_config() in self.app.config: 211 | # if an app was provided in the constructor, search its config first 212 | return self.app.config[self._consumer_key_config()] 213 | 214 | # otherwise, search in the current_app config 215 | return current_app.config.get(self._consumer_key_config(), None) 216 | 217 | @consumer_key.setter 218 | def consumer_key(self, consumer_key): 219 | self.static_consumer_key = consumer_key 220 | 221 | @property 222 | def consumer_secret(self): 223 | ''' 224 | Returns the consumer_secret for this object. A method analogous to that 225 | of `consumer_key` is used to find the value. 226 | ''' 227 | if self.static_consumer_secret is not None: 228 | # if a consumer secret was provided in the constructor, default to that 229 | return self.static_consumer_secret 230 | elif self.app is not None and self._consumer_secret_config() in self.app.config: 231 | # if an app was provided in the constructor, search its config first 232 | return self.app.config[self._consumer_secret_config()] 233 | 234 | # otherwise, search in the current_app config 235 | return current_app.config.get(self._consumer_secret_config(), None) 236 | 237 | @consumer_secret.setter 238 | def consumer_secret(self, consumer_secret): 239 | self.static_consumer_secret = consumer_secret 240 | 241 | def _consumer_key_config(self): 242 | return '%s_CONSUMER_KEY' % (self.name.upper(),) 243 | 244 | def _consumer_secret_config(self): 245 | return '%s_CONSUMER_SECRET' % (self.name.upper(),) 246 | 247 | class RauthOAuth2(OAuth2Service, RauthServiceMixin): 248 | ''' 249 | Encapsulates OAuth 2.0 interaction to be easily integrated with Flask. 250 | 251 | This class inherits :class:`rauth.service.OAuth2Service` and 252 | :class:`RauthServiceMixin`. 253 | 254 | :param app: See :class:`RauthServiceMixin`. 255 | :param base_url: See :class:`RauthServiceMixin`. 256 | :param consumer_key: A static consumer key to use with this service. 257 | Supplying this argument will mean any consumer keys found in Flask 258 | application config will be ignored. 259 | :param consumer_secret: A static consumer secret to use with this service. 260 | Supplying this argument will mean any consumer secrets found in Flask 261 | application config will be ignored. 262 | :param kwargs: Any arguments that can be passed to 263 | :class:`rauth.OAuth2Service`. 264 | ''' 265 | def __init__(self, app=None, base_url=None, consumer_key=None, consumer_secret=None, **kwargs): 266 | RauthServiceMixin.__init__(self, app=app, base_url=base_url) 267 | OAuth2Service.__init__(self, consumer_key=consumer_key, consumer_secret=consumer_secret, **kwargs) 268 | 269 | def authorize(self, callback, **authorize_params): 270 | ''' 271 | Begins the OAuth 2.0 authorization process for this service. 272 | 273 | :param callback: The **required** absolute URL that will be 274 | redirected to by the OAuth 2.0 endpoint after authorization is 275 | complete. 276 | :param authorize_params: Query parameters to be passed to authorization, 277 | prompt, addition to the `redirect_uri`. One common example is 278 | `scope`. 279 | ''' 280 | # save the redirect_uri in the session 281 | session[self._session_key('redirect_uri')] = callback 282 | 283 | return redirect(self.get_authorize_url(redirect_uri=callback, **authorize_params)) 284 | 285 | def authorized_handler(self, method='POST'): 286 | ''' 287 | The decorator to assign a function that will be called after 288 | authorization is complete. By default, a `POST` request is used to 289 | fetch the access token. If you need to send a `GET` request, use the 290 | ``authorized_handler(method='GET')`` to do so. 291 | 292 | It should be a route that takes two parameters: `response` and 293 | `access_token`. 294 | 295 | If `response` is ``access_denied``, then the user denied access to 296 | his/her information. 297 | ''' 298 | def create_authorized_handler(f): 299 | @wraps(f) 300 | def decorated(*args, **kwargs): 301 | resp = access_token = None 302 | if 'error' in request.args and request.args['error'] == ACCESS_DENIED: 303 | resp = ACCESS_DENIED 304 | elif 'code' in request.args: 305 | resp = RauthResponse(self.get_access_token(method=method, data={ 306 | 'code': request.args['code'], 307 | 'redirect_uri': session.pop(self._session_key('redirect_uri'), None) 308 | })) 309 | 310 | if resp.status != 200: 311 | raise RauthException('An error occurred while getting the OAuth 2.0 access_token', resp) 312 | 313 | access_token = resp.content.get('access_token') 314 | 315 | return f(*((resp, access_token) + args), **kwargs) 316 | return decorated 317 | return create_authorized_handler 318 | 319 | def request(self, method, url, access_token=None, **kwargs): 320 | ''' 321 | Make a request using an `access_token` obtained via the 322 | :func:`authorized_handler`. 323 | 324 | If no access_token is provided and a 325 | :func:`RauthServiceMixin.tokengetter` **was** provided, the 326 | :func:`RauthServiceMixin.tokengetter` will be called. 327 | 328 | :param method: Same as :func:`rauth.OAuth2Service.request`. 329 | :param url: Same as :func:`rauth.OAuth2Service.request`, except when a 330 | `base_url` was provided to the constructor, in which case the URL 331 | should be any valid endpoint after being :func:`urljoin` ed with 332 | the `base_url`. 333 | :param access_token: The `access_token` required to make requests 334 | against this service. 335 | :param kwargs: Any `kwargs` that can be passed to 336 | :func:`OAuth2Service.request`. 337 | ''' 338 | url = self._expand_url(url) 339 | 340 | if access_token is None and self.tokengetter_f is not None: 341 | access_token = self.tokengetter_f() 342 | 343 | # add in the access_token 344 | if 'params' not in kwargs: 345 | kwargs['params'] = {'access_token': access_token} 346 | elif 'access_token' not in kwargs['params']: 347 | # TODO: handle if the user sends bytes -> properly append 'access_token' 348 | kwargs['params']['access_token'] = access_token 349 | 350 | # call the parent implementation 351 | return RauthResponse(OAuth2Service.request(self, method, url, **kwargs)) 352 | 353 | class RauthOAuth1(OAuth1Service, RauthServiceMixin): 354 | ''' 355 | Encapsulates OAuth 1.0a interaction to be easily integrated with Flask. 356 | 357 | This class inherits :class:`rauth.service.OAuth1Service` and 358 | :class:`RauthServiceMixin`. 359 | 360 | See :class:`RauthOAuth2` for analogous details. 361 | ''' 362 | def __init__(self, app=None, base_url=None, consumer_key=None, consumer_secret=None, **kwargs): 363 | RauthServiceMixin.__init__(self, app=app, base_url=base_url) 364 | OAuth1Service.__init__(self, consumer_key=consumer_key, consumer_secret=consumer_secret, **kwargs) 365 | 366 | def authorize(self, callback, **request_params): 367 | ''' 368 | Begins the OAuth 1.0a authorization process for this service. 369 | 370 | :param callback: The **required** absolute URL that will be 371 | redirected to by the OAuth 1.0 endpoint after authorization is 372 | complete. 373 | :param request_params: Query parameters to be passed to the request, 374 | token endpoint, in addition to the `callback`. One common example 375 | is `scope`. 376 | ''' 377 | # fetch the request_token (token and secret 2-tuple) and convert it to a dict 378 | request_token = self.get_request_token(oauth_callback=callback, **request_params) 379 | request_token = {'request_token': request_token[0], 'request_token_secret': request_token[1]} 380 | 381 | # save the request_token in the session 382 | session[self._session_key('request_token')] = request_token 383 | 384 | # pass the token and any user-provided parameters 385 | return redirect(self.get_authorize_url(request_token['request_token'])) 386 | 387 | def authorized_handler(self, method='POST'): 388 | ''' 389 | The handler should expect two arguments: `response` and `oauth_token`. 390 | By default, a `POST` request is used to fetch the access token. If you 391 | need to send a `GET` request, use the 392 | ``authorized_handler(method='GET')`` to do so. 393 | 394 | If `response` is ``None`` then the user *most-likely* denied access 395 | to his/her information. Since OAuth 1.0a does not specify a 396 | standard query parameter to specify that the user denied the 397 | authorization, you will need to figure out how the endpoint that 398 | your are interacting with delineates this edge-case. 399 | ''' 400 | def create_authorized_handler(f): 401 | @wraps(f) 402 | def decorated(*args, **kwargs): 403 | resp = oauth_token = None 404 | if 'oauth_verifier' in request.args: 405 | resp = RauthResponse(self.get_access_token( 406 | method=method, 407 | data={'oauth_verifier': request.args['oauth_verifier']}, 408 | **session.pop(self._session_key('request_token'), {})) 409 | ) 410 | 411 | if resp.status != 200: 412 | raise RauthException('An error occurred during OAuth 1.0a authorization', resp) 413 | 414 | oauth_token = (resp.content.get('oauth_token'), resp.content.get('oauth_token_secret')) 415 | 416 | return f(*((resp, oauth_token) + args), **kwargs) 417 | return decorated 418 | return create_authorized_handler 419 | 420 | def request(self, method, url, oauth_token=None, **kwargs): 421 | ''' 422 | Make a request using an `oauth_token` obtained via the 423 | :func:`authorized_handler`. 424 | ''' 425 | url = self._expand_url(url) 426 | 427 | if oauth_token is None and self.tokengetter_f is not None: 428 | oauth_token = self.tokengetter_f() 429 | 430 | # take apart the 2-tuple 431 | if oauth_token is not None: 432 | oauth_token, oauth_token_secret = oauth_token 433 | else: 434 | oauth_token_secret = None 435 | 436 | # call the parent implementation 437 | return RauthResponse(OAuth1Service.request(self, method, url, access_token=oauth_token, access_token_secret=oauth_token_secret, **kwargs)) 438 | 439 | class RauthOfly(OflyService, RauthServiceMixin): 440 | ''' 441 | Encapsulates Ofly interaction to be easily integrated with Flask. 442 | 443 | This class inherits :class:`rauth.service.OflyService` and 444 | :class:`RauthServiceMixin`. 445 | 446 | See :class:`RauthOAuth2` for analogous details. 447 | ''' 448 | def __init__(self, app=None, base_url=None, consumer_key=None, consumer_secret=None, **kwargs): 449 | RauthServiceMixin.__init__(self, app=app, base_url=base_url) 450 | OflyService.__init__(self, consumer_key=consumer_key, consumer_secret=consumer_secret, **kwargs) 451 | 452 | def authorize(self, callback, **authorize_params): 453 | ''' 454 | Begins the Ofly authorization process for this service. 455 | 456 | :param callback: The **required** absolute URL that will be 457 | redirected to by the Ofly endpoint after authorization is 458 | complete. 459 | :param authorize_params: Query parameters to be passed to the request, 460 | token endpoint, in addition to the `callback`. 461 | ''' 462 | # Ofly web authentication (== "app authentication" == "seamless sign-in") requires a redirect_uri value 463 | 464 | # pass the callback and any user-provided parameters 465 | return redirect(self.get_authorize_url(redirect_uri=callback, **authorize_params)) 466 | 467 | def authorized_handler(self, method='POST'): 468 | ''' 469 | The handler should expect two arguments: `response` and `oflyUserid`. 470 | The `method` parameter is unused. 471 | 472 | If `response` is ``access_denied``, then the user denied access to 473 | his/her information. 474 | ''' 475 | def create_authorized_handler(f): 476 | @wraps(f) 477 | def decorated(*args, **kwargs): 478 | resp = oflyUserid = None 479 | if 'oflyUserid' in request.args: 480 | if request.args['oflyUserid'] == 'no-grant': 481 | resp = ACCESS_DENIED 482 | else: 483 | resp = { 484 | 'oflyUserid': request.args['oflyUserid'], 485 | 'oflyAppId': request.args.get('oflyAppId'), 486 | 'oflyUserEmail': request.args.get('oflyUserEmail') 487 | } 488 | 489 | oflyUserid = request.args['oflyUserid'] 490 | 491 | return f(*((resp, oflyUserid) + args), **kwargs) 492 | return decorated 493 | return create_authorized_handler 494 | 495 | def request(self, method, url, oflyUserid=None, **kwargs): 496 | ''' 497 | Make a request using an `oflyUserid` obtained via the 498 | :func:`authorized_handler`. 499 | ''' 500 | url = self._expand_url(url) 501 | 502 | if oflyUserid is None and self.tokengetter_f is not None: 503 | oflyUserid = self.tokengetter_f() 504 | 505 | # add in the access_token 506 | if 'params' not in kwargs: 507 | kwargs['params'] = {'oflyUserid': oflyUserid} 508 | elif 'oflyUserid' not in kwargs['params']: 509 | # TODO: handle if the user sends bytes -> properly append 'oflyUserid' 510 | kwargs['params']['oflyUserid'] = oflyUserid 511 | 512 | # call the parent implementation 513 | return RauthResponse(OflyService.request(self, method, url, **kwargs)) 514 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/ 3 | build-dir = docs/_build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/_build/html 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask-Rauth 3 | ----------- 4 | 5 | Adds OAuth 1.0/a, 2.0, and Ofly consumer support for Flask. 6 | 7 | Links 8 | ````` 9 | 10 | * `development version `_ 11 | * `rauth `_ 12 | """ 13 | from setuptools import setup 14 | 15 | 16 | setup( 17 | name='Flask-Rauth', 18 | version='0.3.2', 19 | url='https://github.com/joelverhagen/flask-rauth', 20 | license='BSD', 21 | author='Joel Verhagen', 22 | author_email='joel.verhagen@gmail.com', 23 | description='Adds OAuth 1.0/a, 2.0, and Ofly consumer support for Flask.', 24 | long_description=__doc__, 25 | py_modules=['flask_rauth'], 26 | zip_safe=False, 27 | platforms='any', 28 | install_requires=[ 29 | 'Flask', 30 | 'rauth' 31 | ], 32 | classifiers=[ 33 | 'Development Status :: 4 - Beta', 34 | 'Environment :: Web Environment', 35 | 'Intended Audience :: Developers', 36 | 'License :: OSI Approved :: BSD License', 37 | 'Operating System :: OS Independent', 38 | 'Programming Language :: Python', 39 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 40 | 'Topic :: Software Development :: Libraries :: Python Modules' 41 | ] 42 | ) 43 | --------------------------------------------------------------------------------