pax_global_header00006660000000000000000000000064145716114010014512gustar00rootroot0000000000000052 comment=d51d115f112f42b3f0848ce7f315726fea995336 flask-dance-7.1.0/000077500000000000000000000000001457161140100136675ustar00rootroot00000000000000flask-dance-7.1.0/.github/000077500000000000000000000000001457161140100152275ustar00rootroot00000000000000flask-dance-7.1.0/.github/FUNDING.yml000066400000000000000000000001431457161140100170420ustar00rootroot00000000000000# https://tidelift.com/subscription/how-to-connect-tidelift-with-github tidelift: pypi/Flask-Dance flask-dance-7.1.0/.github/dependabot.yml000066400000000000000000000002201457161140100200510ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: weekly time: "04:00" open-pull-requests-limit: 10 flask-dance-7.1.0/.github/workflows/000077500000000000000000000000001457161140100172645ustar00rootroot00000000000000flask-dance-7.1.0/.github/workflows/docs.yml000066400000000000000000000015431457161140100207420ustar00rootroot00000000000000name: Docs on: pull_request: branches: - main push: branches: - main jobs: sphinx: runs-on: ubuntu-latest name: "Sphinx" defaults: run: working-directory: docs steps: - name: Install system dependencies run: >- sudo apt-get install -y python3-enchant aspell-en hunspell-en-us working-directory: . - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "3.10" - name: Install dependencies run: pip install -e .[docs] working-directory: . - run: sphinx-build -W -b linkcheck -d _build/doctrees . _build/linkcheck - run: sphinx-build -W -b spelling -d _build/doctrees . _build/spelling - run: sphinx-build -W -b html -d _build/doctrees . _build/html flask-dance-7.1.0/.github/workflows/lint.yml000066400000000000000000000013651457161140100207620ustar00rootroot00000000000000name: Lint on: pull_request: branches: - main push: branches: - main jobs: black: runs-on: ubuntu-latest name: black timeout-minutes: 5 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "3.10" - name: Install black run: pip install black - name: Run black run: black --check . isort: runs-on: ubuntu-latest name: isort timeout-minutes: 5 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "3.10" - name: Install dependencies and isort run: pip install -e . isort - name: Run isort run: isort --check . flask-dance-7.1.0/.github/workflows/test.yml000066400000000000000000000031551457161140100207720ustar00rootroot00000000000000name: Test on: pull_request: branches: - main push: branches: - main jobs: pytest: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.7", "3.x"] flask-version: ["2.0.3", "latest"] database-uri: - "postgresql://testuser:testpw@localhost/testdb" - "sqlite://" name: "pytest: Python ${{ matrix.python-version }}, Flask ${{ matrix.flask-version }}, postgres=${{ contains(matrix.database-uri, 'postgres') }}" services: postgres: image: postgres:latest env: POSTGRES_USER: testuser POSTGRES_PASSWORD: testpw POSTGRES_DB: testdb ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 env: DATABASE_URI: ${{ matrix.database-uri }} steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install specific Flask version if: ${{ matrix.flask-version != 'latest' }} run: pip install Flask==${{ matrix.flask-version }} - name: Install dependencies run: pip install -e .[test] - name: Install psycopg2 if: ${{ contains(matrix.database-uri, 'postgres') }} run: pip install psycopg2 - name: Run pytest run: coverage run -m pytest - name: Upload coverage to Codecov if: ${{ always() }} uses: codecov/codecov-action@v3 with: fail_ci_if_error: false flask-dance-7.1.0/.gitignore000066400000000000000000000002121457161140100156520ustar00rootroot00000000000000*.pyc *.egg-info .DS_Store .coverage .cache .idea/ .vscode/ htmlcov dist docs/_build build venv/ MANIFEST .tox .pytest_cache __pycache__/ flask-dance-7.1.0/.readthedocs.yml000066400000000000000000000020431457161140100167540ustar00rootroot00000000000000# Read the Docs configuration file for Sphinx projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version and other tools you might need build: os: ubuntu-22.04 tools: python: "3.12" # You can also specify other tool versions: # nodejs: "20" # rust: "1.70" # golang: "1.20" # Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/conf.py # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs # builder: "dirhtml" # Fail on all warnings to avoid broken references # fail_on_warning: true # Optionally build your docs in additional formats such as PDF and ePub # formats: # - pdf # - epub # Optional but recommended, declare the Python requirements required # to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - method: pip path: . extra_requirements: - docs flask-dance-7.1.0/CHANGELOG.rst000066400000000000000000000470371457161140100157230ustar00rootroot00000000000000Changelog ========= `unreleased`_ ------------- nothing yet `7.1.0`_ (2024-03-05) --------------------- * Set ``auto_refresh_url`` automatically in ``make_azure_blueprint`` when the ``offline_access`` scope is included, thereby enabling automatic token refresh * Allow returning a custom response from a ``oauth_error`` signal handler. `7.0.1`_ (2024-01-05) --------------------- * Support Werkzeug 3 `7.0.0`_ (2023-05-10) --------------------- * Removed Twitter pre-set configuration * Added Dexcom pre-set configuration * Added support for authorization flow with PKCE_ `6.2.0`_ (2022-10-12) --------------------- * Added ORCID and ORCID sandbox provider * Switched from setuptools to flit_ for packaging `6.1.1`_ (2022-08-22) --------------------- * Switched from ``setup.cfg`` to ``pyproject.toml`` * Added an "install_required" marker to the tests that require this project to be installed before the tests can pass. To run all tests _except_ those, run ``pytest -m "not install_required"``. `6.1.0`_ (2022-08-05) --------------------- * Switched from deprecated ``flask._app_ctx_stack`` to storing app state on ``flask.g`` instead. This should support Flask 2.3.0. * Added OpenStreetMap (OSM) provider `6.0.0`_ (2022-04-05) --------------------- * Added support for Flask 2.1 and Werkzeug 2.1 * Minimum supported version of Flask is now 2.0.3 * Codebase is now linted using `isort`_ `5.1.0`_ (2021-11-01) --------------------- * Added Fitbit pre-set configuration `5.0.0`_ (2020-05-12) --------------------- * Added support for Flask 2.0 and Werkzeug 2.0. * Minimum supported version of Flask is now 1.0.4. * BaseOAuthConsumerBlueprint now accepts a ``rule_kwargs`` parameter, which allows you to configure how the OAuth routes are configured. All of the pre-set configurations have been updated to also accept a ``rule_kwargs`` parameter as well. * The blueprint classes and the pre-set configurations now use keyword-only arguments, as defined in `PEP-3102`_. `4.0.0`_ (2021-04-10) --------------------- * Dropped support for Python 2 and Python 3.5 * If you are using the SQLAlchemy token storage, this project now depends on SQLAlchemy version 1.3.11 and above. ``sqlalchemy-utils`` is no longer necessary. * Added ``verify_tls_certificates`` option to ``make_gitlab_blueprint`` * Added Twitch pre-set configuration `3.3.1`_ (2021-03-01) --------------------- * Added ``hostname`` option to the ``make_salesforce_blueprint`` * Added ``is_sandbox`` option to the ``make_salesforce_blueprint`` * Changed base url for `make_salesforce_blueprint` `3.3.0`_ (2021-02-25) --------------------- * Added Atlassian pre-set configuration * Added Salesforce pre-set configuration * Added ``offline`` option to ``make_dropbox_blueprint`` * Added ``prompt`` option to ``make_discord_blueprint`` * Added ``subdomain`` option to ``make_slack_blueprint`` `3.2.0`_ (2020-11-24) --------------------- Added Digital Ocean pre-set configuration `3.1.0`_ (2020-10-29) --------------------- * Updated Discord to use the new discord.com instead of the old discordapp.com * Add Strava pre-set configuration `3.0.0`_ (2019-10-21) --------------------- * Updated Meetup and Nylas pre-set configurations to include the ``client_id`` in the OAuth token request. * Removed Okta pre-set configuration, since it doesn't add any value over using ``OAuth2ConsumerBlueprint`` directly. * Updated Azure to allow defining ``authorization_url_params`` `2.2.0`_ (2019-06-04) --------------------- * Added Heroku pre-set configuration `2.1.0`_ (2019-05-15) --------------------- * Flask-Dance now provides a ``betamax_record_flask_dance`` testing fixture, for recording and replaying HTTP requests using Betamax_. See the testing documentation for more information. * Added LinkedIn pre-set configuration `2.0.0`_ (2019-03-30) --------------------- Changed (**backwards incompatible**) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * The backwards-compatible references to "backend" have been removed. Use "storage" instead. * The columns defined in ``OAuthConsumerMixin`` now set ``nullable=False``. If you are using the SQLAlchemy storage and are upgrading from a previous version of Flask-Dance, you may want to do a database migration. * Previously, Flask-Dance had an undocumented feature where it would automatically redirect based on a ``next`` parameter in the URL. This undocumented feature has been removed. * All pre-set configurations now use a consistent naming scheme for pulling client IDs and client secrets from the app config. The following configurations have changed: Dropbox, Meetup, Twitter, and Zoho. * Replace ``lazy`` dependency with `werkzeug.utils.cached_property `__ `1.4.0`_ (2019-02-22) --------------------- Changed ~~~~~~~ * "Backends" are now called "Storages", since the word "backend" means something different in the context of web development. This release is fully backwards-compatible, but deprecation warnings have been added anywhere that you import and use a backend (rather than a storage). Added ~~~~~ * Add ``oauth_before_login`` signal * Add ``reprompt_select_account`` parameter to google blueprint `1.3.0`_ (2019-01-14) --------------------- Added ~~~~~ * Add ``authorization_required`` decorator * Added Authentiq pre-set configuration `1.2.0`_ (2018-12-05) --------------------- Added ~~~~~ * Added ``rerequest_declined_permissions`` argument to facebook blueprint * Added Reddit pre-set configuration `1.1.0`_ (2018-09-12) --------------------- Added ~~~~~ * Added ``tenant`` argument to ``make_azure_blueprint`` * Added ``hosted_domain`` argument to ``make_google_blueprint`` * Added Okta pre-set configuration * Added Zoho pre-set configuration Fixed ~~~~~ * Updated Azure AD default scopes. See `issue 149`_. * Only set ``auto_refresh_url`` in ``make_google_blueprint`` if a token of type ``offline`` is requested. See issues `#143`_, `#144`_ and `#161`_ for background. `1.0.0`_ (2018-06-04) ------------------ * Flask-Cache is deprecated. Switch to Flask-Caching. * When using the OAuth 1 blueprint with the SQLAlchemy backend and the ``user_required`` argument set to ``True``, the backend was trying to load tokens before any were set, causing an exception in the backend. Now, the backend will not attempt to load tokens until the OAuth dance is complete. * Added exception handler around ``parse_authorization_response`` in OAuth1 `0.14.0`_ (2018-03-14) ------------------- * Accessing the ``access_token`` property on an instance of the ``OAuth2Session`` class will now query the token backend, instead of checking the client on the instance. * Pre-set configuration for GitLab provider `0.13.0`_ (2017-11-12) ------------------- * sphinxcontrib-napoleon is no longer required to build the Flask-Dance documentation. * Added Spotify pre-set configuration * Added Discord pre-set configuration * Added an optional ``user_required`` argument to the SQLAlchemy backend. When this is enabled, trying to set an OAuth object without an associated user will raise an error. `0.12.0`_ (2017-10-22) ------------------- * Updated the Dropbox configuration to use the v2 authentication URLs * Added the "require_role" authentication parameter for Dropbox * Documented all authentication parameters for Dropbox `0.11.1`_ (2017-07-31) ------------------- * Changed Nylas configuration to refer to "client_id" and "client_secret" rather than "api_id" and "api_secret". `0.11.0`_ (2017-07-24) ------------------- * Added the Nylas pre-set configuration * Improve timezone handling for OAuth 2 token refreshing. * Update tests and docs regarding ``OAuthConsumerMixin`` inheritance. * Fix Dropbox documentation regarding default ``login_url`` and ``authorized_url`` `0.10.1`_ (2016-11-21) ------------------- * Fixed ``make_google_blueprint`` to include ``auto_refresh_url`` so that token renewal is automatically handled by ``requests-oauthlib`` `0.10.0`_ (2016-09-27) ------------------- * Added the Azure AD pre-set configuration * Improve OAuth 2 token auto-refresh `0.9.0`_ (2016-07-1) ----------------- * Allowed an ``oauth_authorized`` event handler to return a ``flask.Response`` instance. If so, that response will be sent to the requesting user. `0.8.3`_ (2016-05-18) ------------------ * Fixed an error that occurred if you were running an unreleased version of Flask, due to the version comparison code. See `issue 53`_. Thanks, @ThiefMaster! `0.8.2`_ (2015-12-30) ------------------ * If the OAuth 1 token request is denied on accessing the login view, Flask-Dance will now redirect the user and fire the ``oauth_error`` signal. This matches the behavior of how Flask-Dance handles OAuth 2 errors. `0.8.1`_ (2015-12-28) ------------------ * Fixed a typo in the Slack configuration, where it would load the OAuth 2 client secret from a config variable named "SLLACK_OAUTH_CLIENT_SECRET" instead of "SLACK_OAUTH_CLIENT_SECRET" `0.8.0`_ (2015-12-28) ------------------ * Added the Slack pre-set configuration * Fixed a subtle bug where setting the ``client_id`` property on an instance of ``OAuth2ConsumerBlueprint`` did not update the value that the ``oauthlib`` library uses to create the redirect URL in the login step. ``client_id`` is now a dynamic property on ``OAuth2ConsumerBlueprint``, which sets the ``client_id`` property on the wrapped ``oauthlib`` client automatically. * Added some debug log statements to ``OAuth2ConsumerBlueprint`` * You can now define a ``session_created`` method on subclasses of ``OAuth2ConsumerBlueprint``. If you do, it will be called when a Requests session is dynamically created, so that the session can be modified before it is returned. `0.7.1`_ (2015-12-12) ------------------ * Removed the Dictective utility class, and replaced it with ``werkzeug.datastructures.CallbackDict``. It does the same thing, but it's better tested, and already a part of one of Flask-Dance's dependencies. * If the user hits the ``authorized`` view without having a "state" variable set in the browser cookies, Flask-Dance will now redirect the user back to the ``login`` view to start the OAuth dance all over again, rather than raising a ``KeyError``. `0.7.0`_ (2015-08-21) ------------------ * Flask-Dance no longer checks for the existence of a ``X-Forwarded-Proto`` header to determine if generated URLs should use a ``https://`` scheme. If you are running your application behind a TLS termination proxy, use Werkzeug's ``ProxyFix`` middleware to inform Flask of that. `0.6.0`_ (2015-05-12) ------------------ * Added the Dropbox pre-set configuration * Added the Meetup pre-set configuration * Added the Facebook pre-set configuration * Flask-Dance now always passes the optional ``redirect_uri`` parameter to the OAuth 2 authorization request, since Dropbox requires it. * Make Flask-Dance provide additional information in errors when providers fail to provide auth tokens `0.5.1`_ (2015-04-28) ------------------ * Make the ``authorized`` property on both ``OAuth1Session`` and ``OAuth2Session`` dynamically load the token from the backend `0.5.0`_ (2015-04-20) ------------------ * Redesigned token storage backend system: it now uses objects .. warning:: This release is not backwards-compatible, due to the changes to how backends work. If you are using the SQLAlchemy backend, read the documentation to see how it works now! * Added documentation about OAuth protocol * Added quickstarts for Google, and for a multi-user SQLAlchemy system * Added ``reprompt_consent`` parameter to Google pre-set configuration * Added ``oauth_error`` signal * If there is an error with the OAuth 2 authorization process, Flask-Dance will now redirect the user anyway rather than letting the error bubble up and cause a 500 status code. The ``oauth_error`` signal will be fired with information about the error. `0.4.3`_ (2015-03-09) ------------------ * ``OAuth2ConsumerBlueprint`` now accepts two new arguments to its constructor: ``authorization_url_params`` and ``token_url_params`` * When using the Google pre-set configuration, you can now request offline access for your OAuth token by passing ``offline=True`` to the ``make_google_blueprint`` function `0.4.2`_ (2015-03-01) ------------------ * Added ``anon_user`` argument to ``set_token_storage_sqlalchemy()`` method * Fire ``oauth_authorized`` signal before setting token, so that a signal handler can set the logged-in user * You can now indicate that an OAuth token should not be stored by returning ``False`` from any receiver function that is connected to the ``oauth_authorized`` signal `0.4.1`_ (2015-02-28) ------------------ * ``OAuth1SessionWithBaseURL`` has been renamed to ``OAuth1Session``. The old name still exists as an alias, for backwards compatibility. * ``OAuth2SessionWithBaseURL`` has been renamed to ``OAuth2Session``. The old name still exists as an alias, for backwards compatibility. * You can now pass a ``user`` or ``user_id`` object to ``blueprint.load_token``. * ``OAuth1Session`` and ``OAuth2Session`` now store a reference to the blueprint, so that you can also call ``session.load_token``, which is proxied to the blueprint. This method also takes ``user`` or ``user_id`` arguments. `0.4.0`_ (2015-02-12) ------------------ * Renamed ``assign_token_to_session`` to ``load_token`` * Added a ``from_config`` dict to OAuthConsumerBlueprint objects. The info in that dict is used to dynamically populate information on the blueprint at runtime from the configuration of the app that the blueprint is bound to. Also set up sensible configuration variable names for the pre-set configurations. * If neither ``redirect_url`` nor ``redirect_to`` are specified, default to redirecting the user to the root of the website (``/``). Previously, specifying one of these two options was required. `0.3.2`_ (2015-01-06) ------------------ * Added a the Google pre-set configuration. `0.3.1`_ (2014-12-16) ------------------ * Added a new ``session_class`` parameter, so that you can specify a custom requests.Session subclass with custom behavior. `0.3.0`_ (2014-12-15) ------------------ * Changed ``OAuthConsumerMixin.created_on`` to ``OAuthConsumerMixin.created_at``, to reflect the fact that it is a DateTime, not a Date. If you are upgrading from an older version of Flask-Dance and using ``OAuthConsumerMixin``, this will require a database migration. `0.2.3`_ (2014-10-13) ------------------ * Renamed ``OAuthMixin`` to ``OAuthConsumerMixin`` `0.2.2`_ (2014-10-13) ------------------ * Changed event sender from app to blueprint, to match docs `0.2.1`_ (2014-10-13) ------------------ * Fixed packaging problems `0.2`_ (2014-10-12) ---------------- * Added SQLAlchemy support * Added Sphinx-based documentation * Added support for Flask-Login and Flask-Cache * Switch from ``login_callback`` decorator to blinker signals `0.1`_ (2014-09-15) ---------------- * Initial release .. _Betamax: https://betamax.readthedocs.io/ .. _`PEP-3102`: https://www.python.org/dev/peps/pep-3102/ .. _issue 53: https://github.com/singingwolfboy/flask-dance/issues/53 .. _issue 149: https://github.com/singingwolfboy/flask-dance/issues/149 .. _#143: https://github.com/singingwolfboy/flask-dance/issues/143 .. _#144: https://github.com/singingwolfboy/flask-dance/issues/144 .. _#161: https://github.com/singingwolfboy/flask-dance/issues/161 .. _isort: https://pycqa.github.io/isort/ .. _flit: https://flit.pypa.io/ .. _PKCE: https://www.rfc-editor.org/rfc/rfc7636 .. _unreleased: https://github.com/singingwolfboy/flask-dance/compare/v7.1.0...HEAD .. _7.0.1: https://github.com/singingwolfboy/flask-dance/compare/v7.0.1...v7.1.0 .. _7.0.1: https://github.com/singingwolfboy/flask-dance/compare/v7.0.0...v7.0.1 .. _7.0.0: https://github.com/singingwolfboy/flask-dance/compare/v6.2.0...v7.0.0 .. _6.2.0: https://github.com/singingwolfboy/flask-dance/compare/v6.1.1...v6.2.0 .. _6.1.1: https://github.com/singingwolfboy/flask-dance/compare/v6.1.0...v6.1.1 .. _6.1.0: https://github.com/singingwolfboy/flask-dance/compare/v6.0.0...v6.1.0 .. _6.0.0: https://github.com/singingwolfboy/flask-dance/compare/v5.1.0...v6.0.0 .. _5.1.0: https://github.com/singingwolfboy/flask-dance/compare/v5.0.0...v5.1.0 .. _5.0.0: https://github.com/singingwolfboy/flask-dance/compare/v4.0.0...v5.0.0 .. _4.0.0: https://github.com/singingwolfboy/flask-dance/compare/v3.3.1...v4.0.0 .. _3.3.1: https://github.com/singingwolfboy/flask-dance/compare/v3.3.0...v3.3.1 .. _3.3.0: https://github.com/singingwolfboy/flask-dance/compare/v3.2.0...v3.3.0 .. _3.2.0: https://github.com/singingwolfboy/flask-dance/compare/v3.1.0...v3.2.0 .. _3.1.0: https://github.com/singingwolfboy/flask-dance/compare/v3.0.0...v3.1.0 .. _3.0.0: https://github.com/singingwolfboy/flask-dance/compare/v2.2.0...v3.0.0 .. _2.2.0: https://github.com/singingwolfboy/flask-dance/compare/v2.1.0...v2.2.0 .. _2.1.0: https://github.com/singingwolfboy/flask-dance/compare/v2.0.0...v2.1.0 .. _2.0.0: https://github.com/singingwolfboy/flask-dance/compare/v1.4.0...v2.0.0 .. _1.4.0: https://github.com/singingwolfboy/flask-dance/compare/v1.3.0...v1.4.0 .. _1.3.0: https://github.com/singingwolfboy/flask-dance/compare/v1.2.0...v1.3.0 .. _1.2.0: https://github.com/singingwolfboy/flask-dance/compare/v1.1.0...v1.2.0 .. _1.1.0: https://github.com/singingwolfboy/flask-dance/compare/v1.0.0...v1.1.0 .. _1.0.0: https://github.com/singingwolfboy/flask-dance/compare/v0.14.0...v1.0.0 .. _0.14.0: https://github.com/singingwolfboy/flask-dance/compare/v0.13.0...v0.14.0 .. _0.13.0: https://github.com/singingwolfboy/flask-dance/compare/v0.12.0...v0.13.0 .. _0.12.0: https://github.com/singingwolfboy/flask-dance/compare/v0.11.1...v0.12.0 .. _0.11.1: https://github.com/singingwolfboy/flask-dance/compare/v0.11.0...v0.11.1 .. _0.11.0: https://github.com/singingwolfboy/flask-dance/compare/v0.10.0...v0.11.0 .. _0.10.1: https://github.com/singingwolfboy/flask-dance/compare/v0.10.0...v0.10.1 .. _0.10.0: https://github.com/singingwolfboy/flask-dance/compare/v0.9.0...v0.10.0 .. _0.9.0: https://github.com/singingwolfboy/flask-dance/compare/v0.8.3...v0.9.0 .. _0.8.3: https://github.com/singingwolfboy/flask-dance/compare/v0.8.2...v0.8.3 .. _0.8.2: https://github.com/singingwolfboy/flask-dance/compare/v0.8.1...v0.8.2 .. _0.8.1: https://github.com/singingwolfboy/flask-dance/compare/v0.8.0...v0.8.1 .. _0.8.0: https://github.com/singingwolfboy/flask-dance/compare/v0.7.1...v0.8.0 .. _0.7.1: https://github.com/singingwolfboy/flask-dance/compare/v0.7.0...v0.7.1 .. _0.7.0: https://github.com/singingwolfboy/flask-dance/compare/v0.6.0...v0.7.0 .. _0.6.0: https://github.com/singingwolfboy/flask-dance/compare/v0.5.1...v0.6.0 .. _0.5.1: https://github.com/singingwolfboy/flask-dance/compare/v0.5.0...v0.5.1 .. _0.5.0: https://github.com/singingwolfboy/flask-dance/compare/v0.4.3...v0.5.0 .. _0.4.3: https://github.com/singingwolfboy/flask-dance/compare/v0.4.2...v0.4.3 .. _0.4.2: https://github.com/singingwolfboy/flask-dance/compare/v0.4.1...v0.4.2 .. _0.4.1: https://github.com/singingwolfboy/flask-dance/compare/v0.4.0...v0.4.1 .. _0.4.0: https://github.com/singingwolfboy/flask-dance/compare/v0.3.2...v0.4.0 .. _0.3.2: https://github.com/singingwolfboy/flask-dance/compare/v0.3.1...v0.3.2 .. _0.3.1: https://github.com/singingwolfboy/flask-dance/compare/v0.3.0...v0.3.1 .. _0.3.0: https://github.com/singingwolfboy/flask-dance/compare/v0.2.3...v0.3.0 .. _0.2.3: https://github.com/singingwolfboy/flask-dance/compare/v0.2.2...v0.2.3 .. _0.2.2: https://github.com/singingwolfboy/flask-dance/compare/v0.2.1...v0.2.2 .. _0.2.1: https://github.com/singingwolfboy/flask-dance/compare/v0.2...v0.2.1 .. _0.2: https://github.com/singingwolfboy/flask-dance/compare/v0.1...v0.2 .. _0.1: https://github.com/singingwolfboy/flask-dance/compare/9b458e401a0...v0.1 flask-dance-7.1.0/LICENSE000066400000000000000000000021041457161140100146710ustar00rootroot00000000000000The MIT License (MIT) Copyright © 2014-2024 David Baumgold Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. flask-dance-7.1.0/MANIFEST.in000066400000000000000000000002451457161140100154260ustar00rootroot00000000000000include LICENSE include *.rst *.txt recursive-include tests *.py *.txt recursive-include docs * recursive-exclude docs .git global-exclude .git .gitignore .DS_Store flask-dance-7.1.0/README.rst000066400000000000000000000112131457161140100153540ustar00rootroot00000000000000Flask-Dance |build-status| |coverage-status| |docs| =================================================== Doing the OAuth dance with style using Flask, requests, and oauthlib. Currently, only OAuth consumers are supported, but this project could easily support OAuth providers in the future, as well. The `full documentation for this project is hosted on ReadTheDocs `_, including the full list of `supported OAuth providers`_, but this README will give you a taste of the features. Installation ============ Just the basics: .. code-block:: bash $ pip install Flask-Dance Or if you're planning on using the `SQLAlchemy`_ storage: .. code-block:: bash $ pip install Flask-Dance[sqla] Quickstart ========== If you want your users to be able to log in to your app from any of the `supported OAuth providers`_, you've got it easy. Here's an example using GitHub: .. code-block:: python from flask import Flask, redirect, url_for from flask_dance.contrib.github import make_github_blueprint, github app = Flask(__name__) app.secret_key = "supersekrit" blueprint = make_github_blueprint( client_id="my-key-here", client_secret="my-secret-here", ) app.register_blueprint(blueprint, url_prefix="/login") @app.route("/") def index(): if not github.authorized: return redirect(url_for("github.login")) resp = github.get("/user") assert resp.ok return "You are @{login} on GitHub".format(login=resp.json()["login"]) If you're itching to try it out, check out the `flask-dance-github`_ example repository, with detailed instructions for how to run this code. The ``github`` object is a `context local`_, just like ``flask.request``. That means that you can import it in any Python file you want, and use it in the context of an incoming HTTP request. If you've split your Flask app up into multiple different files, feel free to import this object in any of your files, and use it just like you would use the ``requests`` module. You can also use Flask-Dance with any OAuth provider you'd like, not just the pre-set configurations. `See the documentation for how to use other OAuth providers. `_ .. _flask-dance-github: https://github.com/singingwolfboy/flask-dance-github .. _context local: http://flask.pocoo.org/docs/latest/quickstart/#context-locals Storages ======== By default, OAuth access tokens are stored in Flask's session object. This means that if the user ever clears their browser cookies, they will have to go through the OAuth dance again, which is not good. You're better off storing access tokens in a database or some other persistent store, and Flask-Dance has support for swapping out the token storage. For example, if you're using `SQLAlchemy`_, set it up like this: .. code-block:: python from flask_sqlalchemy import SQLAlchemy from flask_dance.consumer.storage.sqla import OAuthConsumerMixin, SQLAlchemyStorage db = SQLAlchemy() class User(db.Model): id = db.Column(db.Integer, primary_key=True) # ... other columns as needed class OAuth(OAuthConsumerMixin, db.Model): user_id = db.Column(db.Integer, db.ForeignKey(User.id)) user = db.relationship(User) # get_current_user() is a function that returns the current logged in user blueprint.storage = SQLAlchemyStorage(OAuth, db.session, user=get_current_user) The SQLAlchemy storage seamlessly integrates with `Flask-SQLAlchemy`_, as well as `Flask-Login`_ for user management, and `Flask-Caching`_ for caching. Full Documentation ================== This README provides just a taste of what Flask-Dance is capable of. To see more, `read the documentation on ReadTheDocs `_. .. _supported OAuth providers: https://flask-dance.readthedocs.io/en/latest/providers.html .. _SQLAlchemy: http://www.sqlalchemy.org/ .. _Flask-SQLAlchemy: http://pythonhosted.org/Flask-SQLAlchemy/ .. _Flask-Login: https://flask-login.readthedocs.io/ .. _Flask-Caching: https://flask-caching.readthedocs.io/ .. |build-status| image:: https://github.com/singingwolfboy/flask-dance/workflows/Test/badge.svg :target: https://github.com/singingwolfboy/flask-dance/actions?query=workflow%3ATest :alt: Build status .. |coverage-status| image:: http://codecov.io/github/singingwolfboy/flask-dance/coverage.svg?branch=main :target: http://codecov.io/github/singingwolfboy/flask-dance?branch=main :alt: Test coverage .. |docs| image:: https://readthedocs.org/projects/flask-dance/badge/?version=latest&style=flat :target: http://flask-dance.readthedocs.io/ :alt: Documentation flask-dance-7.1.0/docs/000077500000000000000000000000001457161140100146175ustar00rootroot00000000000000flask-dance-7.1.0/docs/Makefile000066400000000000000000000155651457161140100162730ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " spellcheck to check spelling" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FlaskDance.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FlaskDance.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/FlaskDance" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FlaskDance" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." spellcheck: $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling @echo @echo "Spelling check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/spelling/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." flask-dance-7.1.0/docs/_static/000077500000000000000000000000001457161140100162455ustar00rootroot00000000000000flask-dance-7.1.0/docs/_static/flask-dance.pdf000066400000000000000000001263601457161140100211200ustar00rootroot00000000000000%PDF-1.5 % 3 0 obj << /Length 4 0 R /Filter /FlateDecode >> stream xTMG qiL|HrdՆ%" ~~{`'*l?.7 c 8&J!>?ェۍRohc iLtay~gz5?͖$,}2ΎF]'gvzrQ!y,υײ}zB z7KjSLNQl8CџvGQ\ \p|34/ ngCU:i4PjCHww.!kaآop $kYi9} 0*f"Z2Q*,JFʳgcYF#/"]jSxGS <{ieCR[rΣ]5,QǺ$ 9q:ƺKj2b\-_^~) Fӫw?ۼ9#t3\'1%vD~y}>}j_g m[=2rzq/"^YS endstream endobj 4 0 obj 792 endobj 2 0 obj << /ExtGState << /a0 << /CA 1 /ca 1 >> >> /XObject << /x5 5 0 R /x6 6 0 R /x7 7 0 R /x8 8 0 R /x9 9 0 R >> /Font << /f-0-0 10 0 R /f-1-0 11 0 R >> >> endobj 12 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 1440 560 ] /Contents 3 0 R /Group << /Type /Group /S /Transparency /I true /CS /DeviceRGB >> /Resources 2 0 R >> endobj 13 0 obj << /Length 14 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 432 /Height 594 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 8 >> stream x݉fi(iދkߊKpm%]_\)(]$"ڤEh/i4hiW6͌fi9yϳ|9σssB@oǃ?mvc͎OY~ajܾȔ#e2 K S뱏({[qW)7VmXNcV;V6e%e<^K?~In>W_G.Oƽ~?X{m{8";]C?jD`k}2X%lz~$g3bR ؓ/D?.Vo}8x[6Ij\h>ָc*!9pֈ]*+f5U;5ԍVn(?KSC ţUdc\`"M'7=ց~vC|Yr_4MFh^Z_hXAΟVрu+zEw0CŞ {Naq: >:W;]ۚkbRýTzv<&;W%}@G׭Z|/!n:kŵ0zMLC2:JTOK>O% }ELß-7x[n`FEFAHxE_ObZTΤ9XĥIݙZ.y@īJ h;Q`QE:-q@=^8ϨxSfax5NF߳ hJc>9q8ޏt6L?:/y = k:3 U"'k s ߙ[gMSᆳtfy<u~]N3t@.~dJTPcӹ=NItmCWT t5iFT錪6aoidOK*q2:()U5,Lgc*П,0:|Y%_tMI7iS\UtO?'_tQ&ЁE;N*t^^#o!אk TK_A$0*MWC5UeiHJrV:]VT:ͤJ#]VtWYPMZ=ϒaV ظlU=cxL wMVYnM\龂ݯ=CXvI+蠒}C*Qx%N,tNڱdS!tcji~16%S6=o(/]Ys>:(6RkN5DTb{- >0nb$MK1-HW:"؀K^|JvFy;NgT :?ͻ_[UiAQhF2Bpw)Gc* kԎy?:wPU z;tAΠ{ЕyýV~)}uǯ6sV]:x0逊~ؗWxq/]%N1L}ҽ=sXu*]ޟ.t8_M ݍ$]ޗFn~U.uޗt7$/NǛtz?9$&t{?phT}q[ ,ޏ1t5̎t{r$bTo(]އ9[> qV&;m3]c!@[GGGνͯM{Wk)c1ԍt3P)t}ϪBGxct3Rwf?΢{vV:h]߻Ho=Knʹt4wH=p({֞nF2p[)z٥ή.w-34a2ɼ*[H;)e8݌ ]߳t3R:gHk=I7# {Ee(hJ석t4~nF2oj0(4^Бր't5t{?&8O}HtV=VUpFx}w1pSJ0ʟl̿8&F1{lzF\NEFqv_cty\qyeU?p~txmJѥTuW>l+t#]aZMmtS?WqݷvCr{//HG`,O|KS/żQGS9yz3؃&T+0qAS~0}kNf:P136n4.`5.TtZe^%̣":w` * kn\F::kҵNg:uvI50N+*%ZtFeS q;Q ..KK*.ƕþԣKіH': /!՘_-tI%;҃Nķtfa)UqY'cBWDž*Wס+ Sw:/ӕŹn@s`z#ԃҕ)tc5[e DQMsJ׎N,B:lS+҉Eiazۍ_/_鞲 Ն)٢taޤʕך,P\c龢Bjyt_l^3 !:Ϫn:pV`VjlJD:pM%*hO;U fU&hQ%UakIW{$:UҘn+p:4ieNg%ņJU oۈ#,/*ɂR\K#.H{բQo]VtXI^aߏ|Eb'RZ: yf_=C :_YGatVY*JA:4meDWK?ՠJSsWUiGǕO/ź +֫Yx͛tSfufz[ 2VБN*Su̿KԬ+?I̍RձtQ^ >{rݺ.,ևtP>5Ö{8Ʈ7X=Q=eE'j8S"tStb@tc9H'iU7%O+OsJԢ?kg/)]I͉':u#R:|ma+3nwteaKTQYm7-UCg%R΢zu~̢Z#3΂l.-Pk СFKat`ڰ%9FOti!C*Ғ.-F/*v\5M出 N-ғ莪|HatFU[ P` le(NZc}yu$ה#ſ`M<_. ;C5_QfMvſX=UG%G7Dk sFY+H(g?uf"WT٩LBUlw:D/rX`1Bƪf+ g%@-] 6;Y?+ε3Oǫ|&fvjݝ^mI=mE\z/MCzXVLʍf~D PMoCƋ<;5ϕ5g̖V2^FRe64tәS*KyIyP9IY{vj+kz"R5FŕBFBӤ_ʩ5Ę/WZ#).oveک IyhiD]e!kK6te>;s׶G}IBWWHO Ns7XpE"&y j-u I?E=\ȏHdyxG %,%%o6-MLk%K{r-pcKY$"VyDVI8^ UW$ՕtWv!vrf`-uG98'"2D 8HdacӅHd U*#Dדi7*cWv 1B߆aO9`󄮓m+:Ti:=Rl7t@t ܝIAa^bMt#T3s[ƒ5$&gNC=8e|΢k*p6L鑉ft]WfBOzTbXx9G;O fj5QʶBzMHyrzrGW<;@/|cш:t&]]߻whޥ:w!=˾nFKlTw߫N#zUt2tX}ף&MSt0aҽ`z\ >8rd)2jӹpFmJG3?B! ot, uJQ[jUȸNcF҃).su+e s聈t(M̤G".wНt=q ߼FE<7L8`IuMHOFޢ;z4yj(= ߼yro^ŵWqw[UB$ ߼JJL$W)A.Y:FУUAwާ%aKD3FKwZt=U%nuWЯ;E[cSUPKs'zxJ x4xŢWllGW9ZӶʧR5w>C\xڜ9\C^NKb2=R{ԧ;qz̬Cg0F`E"ӏ+qr Ij fXgo_5p x$S{j;jWCslhI?"Õz)ܦdZ)_)#܏׊kã/J+rMWs|s,[هd^U3\k[Oj*Fk3EQk뗮t#Gsjt3}?L?mjJr= .Y3S fN;eVƇGfG6 M觲ùxΜ/p'-ڬ5\)O2M/BɯZ뇧R봏Vݩ=/_k;|O{Bn&OaxpI<\曶f. ܧ)jkʠ GVVP,ݭycxjbʒU gO9/iqfBbgD^ endstream endobj 14 0 obj 6575 endobj 5 0 obj << /Length 15 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 432 /Height 594 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 1 /SMask 13 0 R >> stream x1 Om O}L endstream endobj 15 0 obj 54 endobj 16 0 obj << /Length 17 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 432 /Height 594 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 8 >> stream x݉fi(iދkߊKpm%]_\)(]$"ڤEh/i4hiW6͌fi9yϳ|9σssB@oǃ?mvc͎OY~ajܾȔ#e2 K S뱏({[qW)7VmXNcV;V6e%e<^K?~In>W_G.Oƽ~?X{m{8";]C?jD`k}2X%lz~$g3bR ؓ/D?.Vo}8x[6Ij\h>ָc*!9pֈ]*+f5U;5ԍVn(?KSC ţUdc\`"M'7=ց~vC|Yr_4MFh^Z_hXAΟVрu+zEw0CŞ {Naq: >:W;]ۚkbRýTzv<&;W%}@G׭Z|/!n:kŵ0zMLC2:JTOK>O% }ELß-7x[n`FEFAHxE_ObZTΤ9XĥIݙZ.y@īJ h;Q`QE:-q@=^8ϨxSfax5NF߳ hJc>9q8ޏt6L?:/y = k:3 U"'k s ߙ[gMSᆳtfy<u~]N3t@.~dJTPcӹ=NItmCWT t5iFT錪6aoidOK*q2:()U5,Lgc*П,0:|Y%_tMI7iS\UtO?'_tQ&ЁE;N*t^^#o!אk TK_A$0*MWC5UeiHJrV:]VT:ͤJ#]VtWYPMZ=ϒaV ظlU=cxL wMVYnM\龂ݯ=CXvI+蠒}C*Qx%N,tNڱdS!tcji~16%S6=o(/]Ys>:(6RkN5DTb{- >0nb$MK1-HW:"؀K^|JvFy;NgT :?ͻ_[UiAQhF2Bpw)Gc* kԎy?:wPU z;tAΠ{ЕyýV~)}uǯ6sV]:x0逊~ؗWxq/]%N1L}ҽ=sXu*]ޟ.t8_M ݍ$]ޗFn~U.uޗt7$/NǛtz?9$&t{?phT}q[ ,ޏ1t5̎t{r$bTo(]އ9[> qV&;m3]c!@[GGGνͯM{Wk)c1ԍt3P)t}ϪBGxct3Rwf?΢{vV:h]߻Ho=Knʹt4wH=p({֞nF2p[)z٥ή.w-34a2ɼ*[H;)e8݌ ]߳t3R:gHk=I7# {Ee(hJ석t4~nF2oj0(4^Бր't5t{?&8O}HtV=VUpFx}w1pSJ0ʟl̿8&F1{lzF\NEFqv_cty\qyeU?p~txmJѥTuW>l+t#]aZMmtS?WqݷvCr{//HG`,O|KS/żQGS9yz3؃&T+0qAS~0}kNf:P136n4.`5.TtZe^%̣":w` * kn\F::kҵNg:uvI50N+*%ZtFeS q;Q ..KK*.ƕþԣKіH': /!՘_-tI%;҃Nķtfa)UqY'cBWDž*Wס+ Sw:/ӕŹn@s`z#ԃҕ)tc5[e DQMsJ׎N,B:lS+҉Eiazۍ_/_鞲 Ն)٢taޤʕך,P\c龢Bjyt_l^3 !:Ϫn:pV`VjlJD:pM%*hO;U fU&hQ%UakIW{$:UҘn+p:4ieNg%ņJU oۈ#,/*ɂR\K#.H{բQo]VtXI^aߏ|Eb'RZ: yf_=C :_YGatVY*JA:4meDWK?ՠJSsWUiGǕO/ź +֫Yx͛tSfufz[ 2VБN*Su̿KԬ+?I̍RձtQ^ >{rݺ.,ևtP>5Ö{8Ʈ7X=Q=eE'j8S"tStb@tc9H'iU7%O+OsJԢ?kg/)]I͉':u#R:|ma+3nwteaKTQYm7-UCg%R΢zu~̢Z#3΂l.-Pk СFKat`ڰ%9FOti!C*Ғ.-F/*v\5M出 N-ғ莪|HatFU[ P` le(NZc}yu$ה#ſ`M<_. ;C5_QfMvſX=UG%G7Dk sFY+H(g?uf"WT٩LBUlw:D/rX`1Bƪf+ g%@-] 6;Y?+ε3Oǫ|&fvjݝ^mI=mE\z/MCzXVLʍf~D PMoCƋ<;5ϕ5g̖V2^FRe64tәS*KyIyP9IY{vj+kz"R5FŕBFBӤ_ʩ5Ę/WZ#).oveک IyhiD]e!kK6te>;s׶G}IBWWHO Ns7XpE"&y j-u I?E=\ȏHdyxG %,%%o6-MLk%K{r-pcKY$"VyDVI8^ UW$ՕtWv!vrf`-uG98'"2D 8HdacӅHd U*#Dדi7*cWv 1B߆aO9`󄮓m+:Ti:=Rl7t@t ܝIAa^bMt#T3s[ƒ5$&gNC=8e|΢k*p6L鑉ft]WfBOzTbXx9G;O fj5QʶBzMHyrzrGW<;@/|cш:t&]]߻whޥ:w!=˾nFKlTw߫N#zUt2tX}ף&MSt0aҽ`z\ >8rd)2jӹpFmJG3?B! ot, uJQ[jUȸNcF҃).su+e s聈t(M̤G".wНt=q ߼FE<7L8`IuMHOFޢ;z4yj(= ߼yro^ŵWqw[UB$ ߼JJL$W)A.Y:FУUAwާ%aKD3FKwZt=U%nuWЯ;E[cSUPKs'zxJ x4xŢWllGW9ZӶʧR5w>C\xڜ9\C^NKb2=R{ԧ;qz̬Cg0F`E"ӏ+qr Ij fXgo_5p x$S{j;jWCslhI?"Õz)ܦdZ)_)#܏׊kã/J+rMWs|s,[هd^U3\k[Oj*Fk3EQk뗮t#Gsjt3}?L?mjJr= .Y3S fN;eVƇGfG6 M觲ùxΜ/p'-ڬ5\)O2M/BɯZ뇧R봏Vݩ=/_k;|O{Bn&OaxpI<\曶f. ܧ)jkʠ GVVP,ݭycxjbʒU gO9/iqfBbgD^ endstream endobj 17 0 obj 6575 endobj 6 0 obj << /Length 18 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 432 /Height 594 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 1 /SMask 16 0 R >> stream x1 Om O}L endstream endobj 18 0 obj 54 endobj 19 0 obj << /Length 20 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 432 /Height 594 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 8 >> stream x݉fi(iދkߊKpm%]_\)(]$"ڤEh/i4hiW6͌fi9yϳ|9σssB@oǃ?mvc͎OY~ajܾȔ#e2 K S뱏({[qW)7VmXNcV;V6e%e<^K?~In>W_G.Oƽ~?X{m{8";]C?jD`k}2X%lz~$g3bR ؓ/D?.Vo}8x[6Ij\h>ָc*!9pֈ]*+f5U;5ԍVn(?KSC ţUdc\`"M'7=ց~vC|Yr_4MFh^Z_hXAΟVрu+zEw0CŞ {Naq: >:W;]ۚkbRýTzv<&;W%}@G׭Z|/!n:kŵ0zMLC2:JTOK>O% }ELß-7x[n`FEFAHxE_ObZTΤ9XĥIݙZ.y@īJ h;Q`QE:-q@=^8ϨxSfax5NF߳ hJc>9q8ޏt6L?:/y = k:3 U"'k s ߙ[gMSᆳtfy<u~]N3t@.~dJTPcӹ=NItmCWT t5iFT錪6aoidOK*q2:()U5,Lgc*П,0:|Y%_tMI7iS\UtO?'_tQ&ЁE;N*t^^#o!אk TK_A$0*MWC5UeiHJrV:]VT:ͤJ#]VtWYPMZ=ϒaV ظlU=cxL wMVYnM\龂ݯ=CXvI+蠒}C*Qx%N,tNڱdS!tcji~16%S6=o(/]Ys>:(6RkN5DTb{- >0nb$MK1-HW:"؀K^|JvFy;NgT :?ͻ_[UiAQhF2Bpw)Gc* kԎy?:wPU z;tAΠ{ЕyýV~)}uǯ6sV]:x0逊~ؗWxq/]%N1L}ҽ=sXu*]ޟ.t8_M ݍ$]ޗFn~U.uޗt7$/NǛtz?9$&t{?phT}q[ ,ޏ1t5̎t{r$bTo(]އ9[> qV&;m3]c!@[GGGνͯM{Wk)c1ԍt3P)t}ϪBGxct3Rwf?΢{vV:h]߻Ho=Knʹt4wH=p({֞nF2p[)z٥ή.w-34a2ɼ*[H;)e8݌ ]߳t3R:gHk=I7# {Ee(hJ석t4~nF2oj0(4^Бր't5t{?&8O}HtV=VUpFx}w1pSJ0ʟl̿8&F1{lzF\NEFqv_cty\qyeU?p~txmJѥTuW>l+t#]aZMmtS?WqݷvCr{//HG`,O|KS/żQGS9yz3؃&T+0qAS~0}kNf:P136n4.`5.TtZe^%̣":w` * kn\F::kҵNg:uvI50N+*%ZtFeS q;Q ..KK*.ƕþԣKіH': /!՘_-tI%;҃Nķtfa)UqY'cBWDž*Wס+ Sw:/ӕŹn@s`z#ԃҕ)tc5[e DQMsJ׎N,B:lS+҉Eiazۍ_/_鞲 Ն)٢taޤʕך,P\c龢Bjyt_l^3 !:Ϫn:pV`VjlJD:pM%*hO;U fU&hQ%UakIW{$:UҘn+p:4ieNg%ņJU oۈ#,/*ɂR\K#.H{բQo]VtXI^aߏ|Eb'RZ: yf_=C :_YGatVY*JA:4meDWK?ՠJSsWUiGǕO/ź +֫Yx͛tSfufz[ 2VБN*Su̿KԬ+?I̍RձtQ^ >{rݺ.,ևtP>5Ö{8Ʈ7X=Q=eE'j8S"tStb@tc9H'iU7%O+OsJԢ?kg/)]I͉':u#R:|ma+3nwteaKTQYm7-UCg%R΢zu~̢Z#3΂l.-Pk СFKat`ڰ%9FOti!C*Ғ.-F/*v\5M出 N-ғ莪|HatFU[ P` le(NZc}yu$ה#ſ`M<_. ;C5_QfMvſX=UG%G7Dk sFY+H(g?uf"WT٩LBUlw:D/rX`1Bƪf+ g%@-] 6;Y?+ε3Oǫ|&fvjݝ^mI=mE\z/MCzXVLʍf~D PMoCƋ<;5ϕ5g̖V2^FRe64tәS*KyIyP9IY{vj+kz"R5FŕBFBӤ_ʩ5Ę/WZ#).oveک IyhiD]e!kK6te>;s׶G}IBWWHO Ns7XpE"&y j-u I?E=\ȏHdyxG %,%%o6-MLk%K{r-pcKY$"VyDVI8^ UW$ՕtWv!vrf`-uG98'"2D 8HdacӅHd U*#Dדi7*cWv 1B߆aO9`󄮓m+:Ti:=Rl7t@t ܝIAa^bMt#T3s[ƒ5$&gNC=8e|΢k*p6L鑉ft]WfBOzTbXx9G;O fj5QʶBzMHyrzrGW<;@/|cш:t&]]߻whޥ:w!=˾nFKlTw߫N#zUt2tX}ף&MSt0aҽ`z\ >8rd)2jӹpFmJG3?B! ot, uJQ[jUȸNcF҃).su+e s聈t(M̤G".wНt=q ߼FE<7L8`IuMHOFޢ;z4yj(= ߼yro^ŵWqw[UB$ ߼JJL$W)A.Y:FУUAwާ%aKD3FKwZt=U%nuWЯ;E[cSUPKs'zxJ x4xŢWllGW9ZӶʧR5w>C\xڜ9\C^NKb2=R{ԧ;qz̬Cg0F`E"ӏ+qr Ij fXgo_5p x$S{j;jWCslhI?"Õz)ܦdZ)_)#܏׊kã/J+rMWs|s,[هd^U3\k[Oj*Fk3EQk뗮t#Gsjt3}?L?mjJr= .Y3S fN;eVƇGfG6 M觲ùxΜ/p'-ڬ5\)O2M/BɯZ뇧R봏Vݩ=/_k;|O{Bn&OaxpI<\曶f. ܧ)jkʠ GVVP,ݭycxjbʒU gO9/iqfBbgD^ endstream endobj 20 0 obj 6575 endobj 7 0 obj << /Length 21 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 432 /Height 594 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 1 /SMask 19 0 R >> stream x1 Om O}L endstream endobj 21 0 obj 54 endobj 22 0 obj << /Length 23 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 432 /Height 594 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 8 >> stream x݉fi(iދkߊKpm%]_\)(]$"ڤEh/i4hiW6͌fi9yϳ|9σssB@oǃ?mvc͎OY~ajܾȔ#e2 K S뱏({[qW)7VmXNcV;V6e%e<^K?~In>W_G.Oƽ~?X{m{8";]C?jD`k}2X%lz~$g3bR ؓ/D?.Vo}8x[6Ij\h>ָc*!9pֈ]*+f5U;5ԍVn(?KSC ţUdc\`"M'7=ց~vC|Yr_4MFh^Z_hXAΟVрu+zEw0CŞ {Naq: >:W;]ۚkbRýTzv<&;W%}@G׭Z|/!n:kŵ0zMLC2:JTOK>O% }ELß-7x[n`FEFAHxE_ObZTΤ9XĥIݙZ.y@īJ h;Q`QE:-q@=^8ϨxSfax5NF߳ hJc>9q8ޏt6L?:/y = k:3 U"'k s ߙ[gMSᆳtfy<u~]N3t@.~dJTPcӹ=NItmCWT t5iFT錪6aoidOK*q2:()U5,Lgc*П,0:|Y%_tMI7iS\UtO?'_tQ&ЁE;N*t^^#o!אk TK_A$0*MWC5UeiHJrV:]VT:ͤJ#]VtWYPMZ=ϒaV ظlU=cxL wMVYnM\龂ݯ=CXvI+蠒}C*Qx%N,tNڱdS!tcji~16%S6=o(/]Ys>:(6RkN5DTb{- >0nb$MK1-HW:"؀K^|JvFy;NgT :?ͻ_[UiAQhF2Bpw)Gc* kԎy?:wPU z;tAΠ{ЕyýV~)}uǯ6sV]:x0逊~ؗWxq/]%N1L}ҽ=sXu*]ޟ.t8_M ݍ$]ޗFn~U.uޗt7$/NǛtz?9$&t{?phT}q[ ,ޏ1t5̎t{r$bTo(]އ9[> qV&;m3]c!@[GGGνͯM{Wk)c1ԍt3P)t}ϪBGxct3Rwf?΢{vV:h]߻Ho=Knʹt4wH=p({֞nF2p[)z٥ή.w-34a2ɼ*[H;)e8݌ ]߳t3R:gHk=I7# {Ee(hJ석t4~nF2oj0(4^Бր't5t{?&8O}HtV=VUpFx}w1pSJ0ʟl̿8&F1{lzF\NEFqv_cty\qyeU?p~txmJѥTuW>l+t#]aZMmtS?WqݷvCr{//HG`,O|KS/żQGS9yz3؃&T+0qAS~0}kNf:P136n4.`5.TtZe^%̣":w` * kn\F::kҵNg:uvI50N+*%ZtFeS q;Q ..KK*.ƕþԣKіH': /!՘_-tI%;҃Nķtfa)UqY'cBWDž*Wס+ Sw:/ӕŹn@s`z#ԃҕ)tc5[e DQMsJ׎N,B:lS+҉Eiazۍ_/_鞲 Ն)٢taޤʕך,P\c龢Bjyt_l^3 !:Ϫn:pV`VjlJD:pM%*hO;U fU&hQ%UakIW{$:UҘn+p:4ieNg%ņJU oۈ#,/*ɂR\K#.H{բQo]VtXI^aߏ|Eb'RZ: yf_=C :_YGatVY*JA:4meDWK?ՠJSsWUiGǕO/ź +֫Yx͛tSfufz[ 2VБN*Su̿KԬ+?I̍RձtQ^ >{rݺ.,ևtP>5Ö{8Ʈ7X=Q=eE'j8S"tStb@tc9H'iU7%O+OsJԢ?kg/)]I͉':u#R:|ma+3nwteaKTQYm7-UCg%R΢zu~̢Z#3΂l.-Pk СFKat`ڰ%9FOti!C*Ғ.-F/*v\5M出 N-ғ莪|HatFU[ P` le(NZc}yu$ה#ſ`M<_. ;C5_QfMvſX=UG%G7Dk sFY+H(g?uf"WT٩LBUlw:D/rX`1Bƪf+ g%@-] 6;Y?+ε3Oǫ|&fvjݝ^mI=mE\z/MCzXVLʍf~D PMoCƋ<;5ϕ5g̖V2^FRe64tәS*KyIyP9IY{vj+kz"R5FŕBFBӤ_ʩ5Ę/WZ#).oveک IyhiD]e!kK6te>;s׶G}IBWWHO Ns7XpE"&y j-u I?E=\ȏHdyxG %,%%o6-MLk%K{r-pcKY$"VyDVI8^ UW$ՕtWv!vrf`-uG98'"2D 8HdacӅHd U*#Dדi7*cWv 1B߆aO9`󄮓m+:Ti:=Rl7t@t ܝIAa^bMt#T3s[ƒ5$&gNC=8e|΢k*p6L鑉ft]WfBOzTbXx9G;O fj5QʶBzMHyrzrGW<;@/|cш:t&]]߻whޥ:w!=˾nFKlTw߫N#zUt2tX}ף&MSt0aҽ`z\ >8rd)2jӹpFmJG3?B! ot, uJQ[jUȸNcF҃).su+e s聈t(M̤G".wНt=q ߼FE<7L8`IuMHOFޢ;z4yj(= ߼yro^ŵWqw[UB$ ߼JJL$W)A.Y:FУUAwާ%aKD3FKwZt=U%nuWЯ;E[cSUPKs'zxJ x4xŢWllGW9ZӶʧR5w>C\xڜ9\C^NKb2=R{ԧ;qz̬Cg0F`E"ӏ+qr Ij fXgo_5p x$S{j;jWCslhI?"Õz)ܦdZ)_)#܏׊kã/J+rMWs|s,[هd^U3\k[Oj*Fk3EQk뗮t#Gsjt3}?L?mjJr= .Y3S fN;eVƇGfG6 M觲ùxΜ/p'-ڬ5\)O2M/BɯZ뇧R봏Vݩ=/_k;|O{Bn&OaxpI<\曶f. ܧ)jkʠ GVVP,ݭycxjbʒU gO9/iqfBbgD^ endstream endobj 23 0 obj 6575 endobj 8 0 obj << /Length 24 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 432 /Height 594 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 1 /SMask 22 0 R >> stream x1 Om O}L endstream endobj 24 0 obj 54 endobj 25 0 obj << /Length 26 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 432 /Height 594 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 8 >> stream x݉fi(iދkߊKpm%]_\)(]$"ڤEh/i4hiW6͌fi9yϳ|9σssB@oǃ?mvc͎OY~ajܾȔ#e2 K S뱏({[qW)7VmXNcV;V6e%e<^K?~In>W_G.Oƽ~?X{m{8";]C?jD`k}2X%lz~$g3bR ؓ/D?.Vo}8x[6Ij\h>ָc*!9pֈ]*+f5U;5ԍVn(?KSC ţUdc\`"M'7=ց~vC|Yr_4MFh^Z_hXAΟVрu+zEw0CŞ {Naq: >:W;]ۚkbRýTzv<&;W%}@G׭Z|/!n:kŵ0zMLC2:JTOK>O% }ELß-7x[n`FEFAHxE_ObZTΤ9XĥIݙZ.y@īJ h;Q`QE:-q@=^8ϨxSfax5NF߳ hJc>9q8ޏt6L?:/y = k:3 U"'k s ߙ[gMSᆳtfy<u~]N3t@.~dJTPcӹ=NItmCWT t5iFT錪6aoidOK*q2:()U5,Lgc*П,0:|Y%_tMI7iS\UtO?'_tQ&ЁE;N*t^^#o!אk TK_A$0*MWC5UeiHJrV:]VT:ͤJ#]VtWYPMZ=ϒaV ظlU=cxL wMVYnM\龂ݯ=CXvI+蠒}C*Qx%N,tNڱdS!tcji~16%S6=o(/]Ys>:(6RkN5DTb{- >0nb$MK1-HW:"؀K^|JvFy;NgT :?ͻ_[UiAQhF2Bpw)Gc* kԎy?:wPU z;tAΠ{ЕyýV~)}uǯ6sV]:x0逊~ؗWxq/]%N1L}ҽ=sXu*]ޟ.t8_M ݍ$]ޗFn~U.uޗt7$/NǛtz?9$&t{?phT}q[ ,ޏ1t5̎t{r$bTo(]އ9[> qV&;m3]c!@[GGGνͯM{Wk)c1ԍt3P)t}ϪBGxct3Rwf?΢{vV:h]߻Ho=Knʹt4wH=p({֞nF2p[)z٥ή.w-34a2ɼ*[H;)e8݌ ]߳t3R:gHk=I7# {Ee(hJ석t4~nF2oj0(4^Бր't5t{?&8O}HtV=VUpFx}w1pSJ0ʟl̿8&F1{lzF\NEFqv_cty\qyeU?p~txmJѥTuW>l+t#]aZMmtS?WqݷvCr{//HG`,O|KS/żQGS9yz3؃&T+0qAS~0}kNf:P136n4.`5.TtZe^%̣":w` * kn\F::kҵNg:uvI50N+*%ZtFeS q;Q ..KK*.ƕþԣKіH': /!՘_-tI%;҃Nķtfa)UqY'cBWDž*Wס+ Sw:/ӕŹn@s`z#ԃҕ)tc5[e DQMsJ׎N,B:lS+҉Eiazۍ_/_鞲 Ն)٢taޤʕך,P\c龢Bjyt_l^3 !:Ϫn:pV`VjlJD:pM%*hO;U fU&hQ%UakIW{$:UҘn+p:4ieNg%ņJU oۈ#,/*ɂR\K#.H{բQo]VtXI^aߏ|Eb'RZ: yf_=C :_YGatVY*JA:4meDWK?ՠJSsWUiGǕO/ź +֫Yx͛tSfufz[ 2VБN*Su̿KԬ+?I̍RձtQ^ >{rݺ.,ևtP>5Ö{8Ʈ7X=Q=eE'j8S"tStb@tc9H'iU7%O+OsJԢ?kg/)]I͉':u#R:|ma+3nwteaKTQYm7-UCg%R΢zu~̢Z#3΂l.-Pk СFKat`ڰ%9FOti!C*Ғ.-F/*v\5M出 N-ғ莪|HatFU[ P` le(NZc}yu$ה#ſ`M<_. ;C5_QfMvſX=UG%G7Dk sFY+H(g?uf"WT٩LBUlw:D/rX`1Bƪf+ g%@-] 6;Y?+ε3Oǫ|&fvjݝ^mI=mE\z/MCzXVLʍf~D PMoCƋ<;5ϕ5g̖V2^FRe64tәS*KyIyP9IY{vj+kz"R5FŕBFBӤ_ʩ5Ę/WZ#).oveک IyhiD]e!kK6te>;s׶G}IBWWHO Ns7XpE"&y j-u I?E=\ȏHdyxG %,%%o6-MLk%K{r-pcKY$"VyDVI8^ UW$ՕtWv!vrf`-uG98'"2D 8HdacӅHd U*#Dדi7*cWv 1B߆aO9`󄮓m+:Ti:=Rl7t@t ܝIAa^bMt#T3s[ƒ5$&gNC=8e|΢k*p6L鑉ft]WfBOzTbXx9G;O fj5QʶBzMHyrzrGW<;@/|cш:t&]]߻whޥ:w!=˾nFKlTw߫N#zUt2tX}ף&MSt0aҽ`z\ >8rd)2jӹpFmJG3?B! ot, uJQ[jUȸNcF҃).su+e s聈t(M̤G".wНt=q ߼FE<7L8`IuMHOFޢ;z4yj(= ߼yro^ŵWqw[UB$ ߼JJL$W)A.Y:FУUAwާ%aKD3FKwZt=U%nuWЯ;E[cSUPKs'zxJ x4xŢWllGW9ZӶʧR5w>C\xڜ9\C^NKb2=R{ԧ;qz̬Cg0F`E"ӏ+qr Ij fXgo_5p x$S{j;jWCslhI?"Õz)ܦdZ)_)#܏׊kã/J+rMWs|s,[هd^U3\k[Oj*Fk3EQk뗮t#Gsjt3}?L?mjJr= .Y3S fN;eVƇGfG6 M觲ùxΜ/p'-ڬ5\)O2M/BɯZ뇧R봏Vݩ=/_k;|O{Bn&OaxpI<\曶f. ܧ)jkʠ GVVP,ݭycxjbʒU gO9/iqfBbgD^ endstream endobj 26 0 obj 6575 endobj 9 0 obj << /Length 27 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 432 /Height 594 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 1 /SMask 25 0 R >> stream x1 Om O}L endstream endobj 27 0 obj 54 endobj 28 0 obj << /Length 29 0 R /Filter /FlateDecode /Length1 1572 >> stream xT]LU>gfvua;β@g .Zvm"],4%#-1ğ4iEK1m&/M!ij|36gդ'ޓ{o{7޹C004 #%FGdv4M\xa/NNTL0$8: @z2;3UH{a # X.%xi@`SbW7LJ^5 HKP FP; E*- ENKCs_^ScsN90,sR?կ, 5r s3 T|[SJCOVS;L'<슟ek_ꔫ70[}5>iíhw O,AUq(++TyN ޒ^r{fު8Ph;b t?MzMwwuu]؟Ģ󏏌W].\ؚnicrIT#VTQfIa$|Ɵ w9^x28:~*a|C`0FYl|rWP{V5Jupf̣+֤'HWﯼ*D #:UuiV>}%bdK';jw)mo3pƱ`\P pLN(' sP fqO5tUS(>iuG\熔&^E|l]#uo@ؿL܃jx맻Z`"1-NmXjJKLJtrX߬J9E(nx Dvz> stream x]Pn {L8m %P~Ea|w Q*;KwS=fΛsXFpt5G0N[V~=8u8u&%j9;0K#.}-K?8 XۂAKr/* 3wy=osǒ7Ւ4&GdR-Co*c[%&O4)&O` SPCѾl[s%%r^nV,ofYc_ax endstream endobj 31 0 obj 246 endobj 32 0 obj << /Type /FontDescriptor /FontName /RJANHJ+Baskerville /FontFamily (Baskerville) /Flags 32 /FontBBox [ -505 -344 1765 960 ] /ItalicAngle 0 /Ascent 897 /Descent -246 /CapHeight 960 /StemV 80 /StemH 80 /FontFile2 28 0 R >> endobj 10 0 obj << /Type /Font /Subtype /TrueType /BaseFont /RJANHJ+Baskerville /FirstChar 32 /LastChar 115 /FontDescriptor 32 0 R /Encoding /WinAnsiEncoding /Widths [ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 552 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 468 0 0 0 0 0 0 0 0 0 458 239 0 0 0 0 0 0 333 ] /ToUnicode 30 0 R >> endobj 33 0 obj << /Length 34 0 R /Filter /FlateDecode /Length1 4780 >> stream xW[$Wy>KS]wuL}v.Y{Yax 6xbLpHF &7@,gHe' o`E=zvq@^S]?Na"/_|1l?zDz'B+0=tmg'P!==|ߡi~5@5aK<|'j?A}GK3#P}˗BE? c^}my=sMu!!j~a-BK:W k$N-Kʡ<7s+x8sks9sgؙ*>oÛh!xdc=? a$X!i>X -8ҢۙKh y&`47Uk9}ҹ i^X|\adQ9+|zIݻ/uWu)o D[VLT&# I Q3*lq3\r9; {5191j~]3Va3 ~ xs.ؙ#iJ, לb6rL%ccrҖmѝ,jB rݻׄ(}'"INg(Ji=-{fsYxV Q4n}۴^v'e k'-!Q\JRF-hJ*R(7<kLZd ƦB_9͍Nnw:Չ4|ݰ/p.c]Xv)S-a&BSG/&[?\{k!关)7/x qڴB=!=Rnu}'4u.0^vT-/Lyza䬥h8H1?VL*&Y077t@}hʀF6ϐX0;~ +^L*־+d|VL,v1[ם0,b(ŕ}|žY{""r~\4ԁėL|6cΤvOK,khpNrp $T^VI窌dm Q#aP_q>oթƫ"d_c}+:4U:WA( <ֻ*=YK C(6|5l>leXX$ie! 1US81>124,I1 b-3@99OšgɽF 3R4 jb1[7꿶6|Fe@ H^pJaͶt2yf L{Jx4| %j:rf(a@ tR0 =Nr.#G"$ !&P䀑|ʚ\曀 /..>}qq2'g:z @Jj(9~OD'TäÉ )Dqo)8.Mu3VR(^T9x $nhXO6&c0W~yjJS70[~j{+p@vl-b*L}yy0N&ҝ^kkHA^-!۽B:[MuB2XĞB+<VJ&Ӷ(ܯ{lh7|@ _ ȶ[ 8_BmAh f[A6,$Lf۪cbbi5tt&[N 65Sj>XȗLBAYO3<.*Ђza"dǐC:0t }]pX_ė\+˧?5|?S73?? ~׎ہ wd蝇-y/cC|Cӹ4o}!Key|T4,Kt7r×8`:[-G{{{9?~p2 Kwttavnk幃.<(qsp#5KXXCM\=ua~k~~#=w^-0νx^9Xioy{(o o jj;k/z?&U]M7|Ok 3{GO^$/DCndg ?&MxXNo+ЎYM"7& endstream endobj 34 0 obj 3015 endobj 35 0 obj << /Length 36 0 R /Filter /FlateDecode >> stream x]n wu8]:Hu?j `R!C޾NWg|2σ {j:q[Tֱڪtʭ' τA5OOEѺ_ױ>[?Kаƽ*^ASݦH= JVKk\TA3t_ɨo\i(0@19sW+oS/ywj٬ux_k!hx endstream endobj 36 0 obj 245 endobj 37 0 obj << /Type /FontDescriptor /FontName /EOEKLN+RockSalt /FontFamily (Rock Salt) /Flags 32 /FontBBox [ -337 -768 1906 1584 ] /ItalicAngle 0 /Ascent 1584 /Descent -769 /CapHeight 1584 /StemV 80 /StemH 80 /FontFile2 33 0 R >> endobj 11 0 obj << /Type /Font /Subtype /TrueType /BaseFont /EOEKLN+RockSalt /FirstChar 32 /LastChar 110 /FontDescriptor 37 0 R /Encoding /WinAnsiEncoding /Widths [ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 908 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 718 0 724 0 571 0 0 0 0 0 0 0 0 740 ] /ToUnicode 35 0 R >> endobj 1 0 obj << /Type /Pages /Kids [ 12 0 R ] /Count 1 >> endobj 38 0 obj << /Creator (cairo 1.12.16 (http://cairographics.org)) /Producer (cairo 1.12.16 (http://cairographics.org)) >> endobj 39 0 obj << /Type /Catalog /Pages 1 0 R >> endobj xref 0 40 0000000000 65535 f 0000043136 00000 n 0000000906 00000 n 0000000015 00000 n 0000000884 00000 n 0000008135 00000 n 0000015258 00000 n 0000022381 00000 n 0000029504 00000 n 0000036627 00000 n 0000038611 00000 n 0000042755 00000 n 0000001104 00000 n 0000001320 00000 n 0000008111 00000 n 0000008421 00000 n 0000008443 00000 n 0000015234 00000 n 0000015544 00000 n 0000015566 00000 n 0000022357 00000 n 0000022667 00000 n 0000022689 00000 n 0000029480 00000 n 0000029790 00000 n 0000029812 00000 n 0000036603 00000 n 0000036913 00000 n 0000036935 00000 n 0000037970 00000 n 0000037993 00000 n 0000038318 00000 n 0000038341 00000 n 0000039005 00000 n 0000042116 00000 n 0000042140 00000 n 0000042464 00000 n 0000042487 00000 n 0000043202 00000 n 0000043332 00000 n trailer << /Size 40 /Root 39 0 R /Info 38 0 R >> startxref 43385 %%EOF flask-dance-7.1.0/docs/_static/flask-dance.png000066400000000000000000000456421457161140100211360ustar00rootroot00000000000000PNG  IHDR5xe AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs B(x-iTXtXML:com.adobe.xmp www.inkscape.org 90 1 90 tL>IDATxeEga(9H(H 9+5aQWWQHX"HuwwK̛v')S*T T T T T T T T T T T T T T T T T T T T T T T T T T T , 0tjj)"N炏T T T ,0g&𐲺RkSQͫL0*",=Ν{?57\UU8sx/stl&\:|8;r‡rӧ?7"Tjj੩ph{*X@4.מS{]505(=˂z6#@)mJ`Hɢ+pY(imbd&׏rͫ 4Fz-_|l[=%9u6D/5Q5P505h"ޠCKqL:Zhթ UUw8c -/:N~*UU#Հ&j0щt)=mڴXuO~I9#9en)ͨP5P5P50 St6tZm > |cPZ11C{ q~ #袋>{ƌ:(<7/pLPěDi,0}urt Fp$Ùm\\Q3l"u9hNվoUUU5gq.ol˂2^Ӊ<U:G30[.3 Yw װj`!ԀN:GQ3-8/pbӭȜH'I+h c4ޯ|Lӯ^ nVyqyu&N\;T#UU  tnqkzuApr[^2.I3pȃ!V[(!}pp<15+ GWT(|WPcOrGYЌ]]>B@켲D9х>{1OfCdn._Ph NXQChshet2[CB9֥앝Qmص7]mNށ`Zi> a4:{dXfc,TkJzc8z=o~;&Hm:^e0={q_}$oU›G"e׀/hj8v -3fM_'P4-}fwJuDvArĠ,YE ~~~LdX;g~'I!0;~tp~uҰM1S鸟gJ#:$$}o:mMu`[ mlHyG J_1X܀W (t#(_uZ87A6p? WJ^m3`a>A[rp4?"W,C8_PYC+Fa_ Zcʥ/ V(tcH >Mr;r|88I]:{r+39 WJ_Og`T((0 :wi~>/KHN6ja<@پcv^G4tdC1zOskqkh404ấ52y,vu c8Fc eCa81%EӶѱ}>sAځ!1:7>M탆+ ٚ󳜆ol{ⷁ.ۼh8Xbs srbٖrXVLoF3}=JpյW2u DIW}1V^LzopO0D4w[s x) .خEގFz;ЕLJI0p\KũKt d녣9q|1>ь~@^7y%(wZYb/07pBW!ʃx^Zco#%ctħo\>N[zEɹqhp>gXlktں#X8ƚ9N6d؄Mw>(@M h&VI?1a#v <\DxV.ÛqN3c+CT:CW_%Jc )\~ɻF R2s vr өZ\wo/ k/"7saV͖1wOL0pA9szh9I38'n>walNZǴ<:ހj-O9\k Pw'9 ӮEbp8-nlo٠6v=&Fa`"=0݅G+d1ʿ<(ͷg<|,VciHu;imHX|~mx̰<%O]rLڃ2*u`u,k;Gl8HC`+t2yk @$=prx%P ujcuCLZdwi"9G]O~gk7a/Hup᠎\꾄 >[*ێp@e1MjZm5bu†͍}[2◐wS)ohl"'G0/@2 i!kT(V`>? chm>C{^ڼw6$SWH 0$*}@ <&%kD/o؀0H5]y/AD| PG|oIˤrukN(#Lzd4^usv پ,lH0#)ջ_UȴA!+[G.8']`pw^##9y.iR~Нgf"uETs_Vz}v~o$MBp"@2YOկ؈Pt8T(Yd&TO&D̫Vh( .O%4_ idˆFTi8b`aN3he:XDz)#^WM=N:hNTmo"$ ю _|0#bu %=ܓgF0}Pڏ`h*cq-C!uO bzw)~s I?VꕠѶ'n:֪x ӫZ&&Ss q۝A~%W'\}8ʤ|N9+9Ёj,Ytn# {/>HֆXNԵh4x6g^2@Bo}UqW{ hm>6,ʧsw(xA>PH:_Ym){7j?̅Iy#/%\-Kz7u$#͝rxr*E{dΫs=U KF|sAO|I=ەz=QDW6RsBݗ|+f}OA}A3ٶ- }{Dt\)k/>e~<^TV^4'g 1OB%"}`΋/Mj/ꕼRQ~ Qc%5mSϕVd-{{MM=>xvW(#?"ry!g΂ΨrHkrE;EcW7@X+s[< "krESgNkt|9 eD ݊2y HZ(}nYޕzs"`OAyu;;=1h5ݛr[`iyMzG'~uj]?7m.md4$ z؊]Ę??5emT'm~g]e [AƠ'•2R궗r:?}/]MLUS4h!}фUyI^ at?K N4eчsBWIB۲&4hKwӿk݈&7tA%Ks,obiz=qNrB~m%s 4L<-w@4/ⷠg4M@۲p?ٷZ4D'#lY3rPΔ %֋K&׷p92U%&WOPhO:5@uʷ%\q/HoLSglI@xPgLÍ t^71m#ȼ? fXN Gi{RӇW'1%kNati]4x}Z\/d_16er4Nj) (oZ~O.J>D#4q'u f*S6!WØ 7 s:ch9o_BPJH!|LZ,Evg<ǡ??& ҫlٜSrq7^/@xPF͢/GD?iBsBch@DaRn)G 9-Au @l# IVtS p Si>j gɰk]Q Cj2 W` j8k@e,:a>}6-;m3\ i ҍӶUE<ɀJ6 :FjHYOnA;бԾߔW15޴%WBG/KI˯m="-𕺭7ʵ5yx x.z#67_qMD[%OtOP7OTʼno r:%wFs5F 5<8n9E1>Zdt?H4uhp&zL`1S3r|@H4~*3K\guc,ac[u!Du&z7&ЮbCWѿk)k cN㝒󟙬3'䰝:yߖ33|Dv?&$Աţiт\J,dMȾkQ$YŶ(;< d`DVNR[ĥ#&Ѿ0+m :ycڒU 3ϔ1}R!YּUrbi) >u5N2чάne& rz1Bi%ځ2x#%C lb^-249kAjjFxdwI}rZ{荬d=:+V]ywń:4L}X.*7>ˈДq\ku(mC&4 S0f_ ؚ7' WD Kq u.^{>휁sۅkrIcux}ꯃMN :Y#1^:pȑ+L6LN=<CIԗ:>uX!C!*V `Ne2#ῶQ:/fDO}D,!~ڡJm9 ;_o| >,WD8(X7xȯ BC+CV΄A+*LtյpUP!ٴrH e_TP Y g|1:We_ -9yĠdkIo}3LjC=(7َ_p : DwroX& Ynmm{5K\yé֭МXTGRh 7\!n . kp6vH{q/@-  >_Kɥ.ט>m &Ewhd+):#嵮/54~:L3ZGah]AjEa-g>yౌruR_mF;YZW-z/$:830P౤}mhx %` *Zs(G)y |pZ8Wо_~ݛ=9?з _פ/x%#&?!d꼈PgMmQm)~ԹA~- ;$)PeM<-a xRR0Ao7:6ysFT'%F~6}-v[:ъ|MOl7x 3ل$}_2tA v8A>h7\BGAmc T /C~ϫg0aě@Rڐ+v-Ns J^4C}7*I{(x~l?WB/q&JY{Sv:eem?R/eϻydW$N6'SlN'|!]6?lhd􍍯WP=>1'psә^7%43' x,i]؋(}R }PnMҎ@ٶw9Pʫ3R.ŕ\`Of47|׀rD_N눇 iXlܩ˱jG/pfl>pP}zvVw,u0+_X.09 -K_B𽌾pY.lߎ A?TgI>>dߍ<'m&C E>&uO2 ycD]ߛsDE9Iۋq7 +R ^PDi|ɹyjxw\=~=HEBSI+ItfF1^99b&ߗQNpϡ^H%C1>43h~%؀U[ D[!ds=gKL*K}Ar˹llʨ nӖE?&;$xw>+jS 1&TǾK_42a:[s~o=Eߝb|- \D~IWmc:s.O6SĽ̈́WyMjvuMm˧yCA%GNO*j^3xA9Drշ#~ ąIu~mSmG>/x-x "C@ }Ighz*g:@A%W.'l᧤uhj/2 VND@+Z1>|CJ1i؜<1P$8a\4N {B7'ҔWa΂$k} ։P6-Yb'};!(e䟐͗o6ЫWB2Xs4y/Bֱ1,q iꐬV;( 9yiomB`;eC / #g0t/Bt*#>LKZǮRkScQִ6M/ e҇ͩ2چuJ [Vu"$@AZerr⧛]EZGYO?"E6!*8X7~gp[!ƻ4E@9242Ř>{4 _;vh᠝S`놢cBj і$Dz5 1 ZC<,p<;ֹͭnLo(Xc09\^‹]jP!!sj6>K'>k3ȿD0`u-81tEt(]%ܚg4{w}+/2qv'Թ yK Nue h d[_0_ӏ"KNڄoeLD3Y5]A|jCe SyaD4:\%.C6$t ?CZUYބ^ͷoAuwm& ~&ö ̹9ha_ƫE qaډNC-|vl7%M7x"c2}yp}gφq/Cn0?@q#uusOΏPM],+&C3$$@z]}v>> op7׃:Rpg/9m>qxmEg^E4 .^k.huֱMeT}BBH}% ON/؎(\ +A\ F~,+U|f^DiF> 6"\mt` 8vie*i\zVu$x3z iwfW>5ͮCa' *~ ϸ;OU_;_jj`Ҁ+ڬ.I-C܋S"&.ёCB=M ;|:'2OZP^:(< ψ+?ǖ՟F:~з[* Z?k@$d"2YIدgo㴾,A{ak׀x䨑]UUUUw=_ ƙ̭d8}݅ҒC,Fù%Uvf,ʸtMXD6jjj)p$~h*F87îsY/ęH7:yrpuCe?͏xFnhLSUUOQ 4:7CWhсƁԖLܯ~7[`zFPgf<^ $c%X5thoRAN iVwex]+Yl# uCxj 8'p^"0:ah,mi8T]< ^V T <%4`[8 W>>8I J5-3+J+䠨C![l<\ϒW T ,dpbg9Aٵ:AFwCy9wuԹtuNlJ|OԀ4 JV5P5i eKyu!Vϼt&*Axײh(ooy L}%z/ϙN/< ?N\|6MOv ǥ,&T8{yc >ZN$0{q.>[r|2M*W4 IV T Z>2.d{1: gjm&ψ7^|-239g?&9@a40q%XUӲY¦CUgw%\*)^|v`eDcK ɕNVl{> tG+BWthbq7̫ÌjjX 5M!7r~bx5j]XAꗼTy_pE\~o~s*0(; 9j :u|( b7ʇ0 MYض$B@SI6b%uWc5j#R>uO\ d!Htou5Uڪ¡lS͖tZZA}vh:ᶝ؂fFھh@@4ĕP/&m#Jvjo~|]cjB@i*TmCWaS:z39wہPTupv&A۝aiɉ  $gCw\[z+qPgk7OiIog~u2x㉕r( xz6]<8'3NIWymj`rjo,dTG3Ah/_ڸ2X5AC :Xb;UViRYZCdgTdkW504VXEs˩ܦ+M3Ԥ&ݐTT tbR/|rh:8s{Aܤ8KC UUU'ųioϧ{pmgx@]ҳm+3Ux>mܸ+1̻ɴiۚ5p9dɛȫ5Wg-r:񪁪5ݡǖ΍sqnsh?N|c`S+*T T T L ty}!䟕\L晦:jjj`i"Q}pZ:kȟ>B%[ՖZ4n +4Zc3PLͮ?HN33LL1p l"Z|tcZ5P50Y48S; #ϛ\ZphQ5P501L,p̽m;gng@s'hz-;pPU b9,{,:x#w:ߝ3 թѫA®^Xcmb{ 4n?upBa5Z5P50y4lw>gjzs|s,rtJR5P5P5ҁ?[[ٹyWKwc95]5P5i&#{ 2l7>Bk{ UUUS-g]MNëRU LbsD* ?ݎxj"IENDB`flask-dance-7.1.0/docs/_static/flask-dance.svg000066400000000000000000002740401457161140100211450ustar00rootroot00000000000000 image/svg+xml Flask Dance flask-dance-7.1.0/docs/api.rst000066400000000000000000000104501457161140100161220ustar00rootroot00000000000000Developer Interface =================== Consumers --------- .. currentmodule:: flask_dance.consumer An OAuth consumer is a website that allows users to log in with other websites (known as OAuth providers). Once a user has gone through the OAuth dance, the consumer website is allowed to interact with the provider website on behalf of the user. .. autoclass:: OAuth1ConsumerBlueprint(...) .. automethod:: __init__ .. attribute:: session An :class:`~requests_oauthlib.OAuth1Session` instance that automatically loads tokens for the OAuth provider from the token storage. This instance is automatically created the first time it is referenced for each request to your Flask application. .. autoattribute:: storage .. autoattribute:: token .. attribute:: config A special dictionary that holds information about the current state of the application, which the token storage can use to look up the correct OAuth token from storage. For example, in a multi-user system, where each user has their own OAuth token, information about which user is currently logged in for this request is stored in this dictionary. This dictionary is special because it automatically alerts the storage when any attribute in the dictionary is changed, so that the storage's caches are appropriately invalidated. .. attribute:: from_config A dictionary used to dynamically load variables from the :doc:`Flask application config ` into the blueprint at the start of each request. To tell this blueprint to pull configuration from the app, set key-value pairs on this dict. Keys are the name of the local variable to set on the blueprint object, and values are the variable name in the Flask application config. Variable names can be a dotpath. For example:: blueprint.from_config["session.client_id"] = "GITHUB_OAUTH_CLIENT_ID" Which will cause this line to execute at the start of every request:: blueprint.session.client_id = app.config["GITHUB_OAUTH_CLIENT_ID"] .. autoclass:: OAuth2ConsumerBlueprint(...) .. automethod:: __init__ .. attribute:: session An :class:`~requests_oauthlib.OAuth2Session` instance that automatically loads tokens for the OAuth provider from the storage. This instance is automatically created the first time it is referenced for each request to your Flask application. .. autoattribute:: storage .. autoattribute:: token .. attribute:: config A special dictionary that holds information about the current state of the application, which the token storage can use to look up the correct OAuth token from storage. For example, in a multi-user system, where each user has their own OAuth token, information about which user is currently logged in for this request is stored in this dictionary. This dictionary is special because it automatically alerts the storage when any attribute in the dictionary is changed, so that the storage's caches are appropriately invalidated. .. attribute:: from_config A dictionary used to dynamically load variables from the :doc:`Flask application config ` into the blueprint at the start of each request. To tell this blueprint to pull configuration from the app, set key-value pairs on this dict. Keys are the name of the local variable to set on the blueprint object, and values are the variable name in the Flask application config. Variable names can be a dotpath. For example:: blueprint.from_config["session.client_id"] = "GITHUB_OAUTH_CLIENT_ID" Which will cause this line to execute at the start of every request:: blueprint.session.client_id = app.config["GITHUB_OAUTH_CLIENT_ID"] Storages -------- .. autoclass:: flask_dance.consumer.storage.session.SessionStorage(...) :members: :special-members: .. autoclass:: flask_dance.consumer.storage.sqla.SQLAlchemyStorage(...) :members: :special-members: Sessions -------- .. autoclass:: flask_dance.consumer.requests.OAuth1Session :members: token, authorized, authorization_required .. autoclass:: flask_dance.consumer.requests.OAuth2Session :members: token, access_token, authorized, authorization_required flask-dance-7.1.0/docs/concepts.rst000066400000000000000000000122001457161140100171620ustar00rootroot00000000000000Concepts ======== In order to use OAuth and Flask-Dance correctly, there are a few basic concepts you need to understand. This page will summarize these concepts, and provide links for further information. OAuth ----- OAuth is a system that allows two websites to securely share information. Usually (but not always), OAuth is used to allow users to grant permission to share information from one website to another website. An OAuth **provider** is a website that *provides* information about users to other websites. An OAuth **consumer** is a website that requests information from an OAuth provider. Before an OAuth provider will provide information about a user to an OAuth consumer, the provider must ask the user to grant permission to share that information with the consumer. Before an OAuth provider will even speak to a consumer, the consumer must get a **client ID** and **client secret** from that provider. It's a bit like how some website require you to create an account before you can do anything else. OAuth providers do this so that they keep track of which consumers they are sharing information with. If a consumer starts using information to do evil things (like hacking or impersonating users), the provider can use this information to deactivate that consumer's access to the provider's API. After a user grants permission to share their data with a consumer, the provider gives that consumer an **OAuth token**. This token records the fact that the consumer has permission to access the information, and the consumer must provide this token on *every API request*. If the consumer ever loses this token, it has to get a new one, which might involve asking the user for permission again. As a result, OAuth tokens must be stored somewhere and retrieved as necessary. .. warning:: If an attacker manages to steal an OAuth token from a consumer, the attacker can use that token to do evil things to the user that granted permission for that token. This could cause the OAuth provider to deactivate the consumer from their API. Be careful with OAuth tokens, and store them securely! If you don't, you could lose access to your provider's API! User Management --------------- The most well-known use for OAuth is for bypassing user registration: users can "sign in" with a well-known OAuth provider like Google or Facebook in order to avoid creating another username and password. However, this is not the *only* use for OAuth. In fact, it's entirely possible to use OAuth without even having a user management system on your website at all! For example, let's say you want to create a Twitter account that is completely public. Anyone in the world should be able to post a tweet, without any identifying information attached to it. You build a simple website that has a text field and a "submit" button, but no user management system at all. Whenever anyone submits a tweet to your website, it uses the Twitter API to post that tweet to your Twitter account -- which requires using OAuth. Flask-Dance does *not* assume that your website has a user management system. It's easy to use Flask-Dance with a user management system if you want to, though! Read the documentation on :doc:`multi-user` if you plan to use a user management system. Local Accounts vs Provider Accounts ----------------------------------- It's a common misconception that, because a user can log in to your website using OAuth instead of creating a new username/password combination, that means they do not have a user account on your website. This is false. Logging in with OAuth *does create a local user account*, and that user account will have some kind of identifier (or ``user_id``). The ``user_id`` on this local account does *not* have to match that user's ID on Google, or Facebook, or Twitter, or whatever OAuth provider(s) you choose to use. The distinction between a local account and a provider account is especially important when :doc:`implementing logout `. Blueprints ---------- A :doc:`Flask blueprint ` is component of a Flask application. Because Flask-Dance is designed to be the OAuth component of your Flask application, it is built using blueprints. As a result, Flask-Dance supports all the features that any blueprint supports, including registering the blueprint at any URL prefix or subdomain you want, url routing, and custom error handlers. Read the :doc:`Flask documentation about blueprints ` for more information. Signals ------- Flask uses the `blinker`_ library to provide support for :doc:`signals `. Signals allow you to subscribe to certain events that occur in your application, so that you can respond instantly when those events happen. Signals are an important part of Flask-Dance, because they allow you to do whatever custom processing you want in response to certain events. For example, when a user successfully completes the OAuth dance, you probably want to flash a welcome message or kick off some kind of data import task. Signals allow you to do that without modifying the code in Flask-Dance. Read the :doc:`signals page ` for more information. .. _blinker: https://blinker.readthedocs.io flask-dance-7.1.0/docs/conf.py000066400000000000000000000225751457161140100161310ustar00rootroot00000000000000# # Flask Dance documentation build configuration file, created by # sphinx-quickstart on Sat Sep 27 09:47:52 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) from flask_dance import __version__ # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx.ext.napoleon", "sphinxcontrib.seqdiag", "sphinxcontrib.spelling", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Flask Dance" copyright = "2014-2024, David Baumgold" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "flask" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { "index_logo": "flask-dance.png", "github_fork": "singingwolfboy/flask-dance", } # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = ['_themes'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "FlaskDancedoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( "index", "FlaskDance.tex", "Flask Dance Documentation", "David Baumgold", "manual", ) ] # The name of an image file (relative to this directory) to place at the top of # the title page. latex_logo = "_static/flask-dance.png" # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ("index", "flaskdance", "Flask Dance Documentation", ["David Baumgold"], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "FlaskDance", "Flask Dance Documentation", "David Baumgold", "FlaskDance", "One line description of project.", "Miscellaneous", ) ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "flask": ("https://flask.palletsprojects.com/en/latest/", None), "flask_login": ("https://flask-login.readthedocs.io/en/latest/", None), "werkzeug": ("https://werkzeug.palletsprojects.com/en/latest/", None), "requests": ("https://requests.kennethreitz.org/en/latest/", None), "oauthlib": ("https://oauthlib.readthedocs.io/en/latest/", None), "requests_oauthlib": ("https://requests-oauthlib.readthedocs.io/en/latest/", None), "sqlalchemy": ("https://docs.sqlalchemy.org/en/latest/", None), "betamax": ("https://betamax.readthedocs.io/en/latest", None), "pytest": ("https://docs.pytest.org/en/latest/", None), } autodoc_member_order = "bysource" seqdiag_antialias = True seqdiag_fontpath = [ "/usr/share/fonts/Arial Unicode.ttf", "/usr/share/fonts/Arial.ttf", "/Library/Fonts/Arial Unicode.ttf", "/Library/Fonts/Arial.ttf", ] flask-dance-7.1.0/docs/contributing.rst000066400000000000000000000036361457161140100200700ustar00rootroot00000000000000Contributing to Flask-Dance =========================== If you want to contribute to Flask-Dance, we would love the help! Providers --------- The simplest way to contribute to Flask-Dance is by adding new pre-set OAuth providers. Check out the ``contrib`` directory to see how to do that. Don't forget to check the README file to see the requirements for getting your contribution merged! Documentation ------------- Contributing to the documentation is probably the best thing you can do for Flask-Dance! OAuth is complicated, and the people using Flask-Dance don't want to understand all the details. Writing clear, comprehensive documentation will make everyone's lives easier. Code ---- The general checklist for all code contributions is: - Code - Tests - Docs - Changelog We strive for roughly 95% test coverage. Not every line needs to be tested, but there should be a clear justification for untested lines in your pull request. You can use `tox`_ to run the unit tests on multiple Python versions locally, if you want. Documenting changes is very important! Particularly when adding new features or changing existing ones, if it isn't documented, no one will know that it exists. The changelog is important to people who are using an old version of Flask-Dance, and want to upgrade to a more recent version. In your pull request, add a bullet point to the "unreleased" section of the changelog, describing what your change does. Do not modify the version number in your pull request. The maintainer will change the version number when releasing a new version of Flask-Dance. Don't be afraid to ask for help! If you have a code change you'd like to make, and you don't know how to write the tests or the docs, you're welcome to open a pull request anyway and ask for help with completing the remaining steps. (Just don't expect your pull request to be merged until those steps are complete!) .. _tox: https://tox.readthedocs.io/flask-dance-7.1.0/docs/examples.rst000066400000000000000000000011161457161140100171660ustar00rootroot00000000000000Projects Using Flask-Dance ========================== If you want to see how others use Flask-Dance, check out some of these projects. To add a project to this list, first make sure that the project has some documentation and the code is publicly visible. Then send a pull request to the docs section of the GitHub repository! openedx-webhooks ---------------- :source: `@edx/openedx-webhooks on GitHub `_ :providers: GitHub, JIRA The project that created Flask-Dance. This project uses Flask-Dance to synchronize events between GitHub and JIRA. flask-dance-7.1.0/docs/how-oauth-works.rst000066400000000000000000000212241457161140100204300ustar00rootroot00000000000000How OAuth Works =============== Definitions ----------- OAuth uses a series of specially-crafted HTTP views and redirects to allow websites to share information with each other securely, and with the user's consent [#oauth-user]_. There are four roles in an OAuth interaction: provider A website that has information about a user. Well-known OAuth providers include Google, Facebook, GitHub etc. consumer A website that wants to obtain some information about a user from the provider. user An actual person who controls information stored with the provider. client A program (usually a web browser) that interacts with the provider and consumer on behalf of the user. In order to securely interact with each other, the provider and consumer must exchange secrets ahead of time, before any OAuth communication actually happens. Generally, this happens when someone who runs the consumer website goes to the provider website and registers an application with the provider, putting in information about the name and URL of the consumer website. The provider then gives the consumer a "client secret", which is a random string of letters and numbers. By presenting this client secret in future OAuth communication, the provider website can verify that the consumer is who they say they are, and not some other website trying to intercept the communication. .. note:: Even though it is called a "client secret", the secret represents the consumer website, not the client (the user's web browser). After the consumer has registered an application with the provider and gotten a client secret, the consumer can do the "OAuth dance" to get consent from a user to share information with the consumer. There are two different versions of the dance: OAuth 1, which is the original version; and OAuth 2, the successor to OAuth 1 which is more flexible and more widely used today. OAuth 2 ------- .. seqdiag:: seqdiag { Client -> Consumer [label = "start dance"]; Client <- Consumer [label = "redirect to provider,\nwith secret, scopes, & state"]; Client -> Provider [label = "follow redirect", rightnote="user\ngrants\n consent"]; Client <- Provider [label = "redirect to consumer, with authorization code and state"]; Client -> Consumer [label = "follow redirect"]; Consumer --> Provider [label = "send secret and\nauthorization code"]; Consumer <-- Provider [label = "return access token"]; Client <-- Consumer [label = "render page or redirect"]; } 1. The client visits the consumer at a special URL, indicating that they want to connect to the provider with OAuth. Typically, there is a button on the consumer's website labeled "Log In with Google" or similar, which takes the user to this special URL. 2. The consumer decides how much of the user's data they want to access, using specific keywords called "scopes". The consumer also makes up a random string of letters and numbers, called a "state" token. The consumer crafts a special URL that points to the provider, but has the client secret, the scopes, the state token embedded in it. The consumer asks the client to visit the provider using this special URL. 3. When the client visits the provider at that URL, the provider notices the client secret, and looks up the consumer that it belongs to. The provider also notices the scopes that the consumer is requesting. The provider displays a page informing the user what information the consumer wants access to -- it may be all of the user's information, or just some of the user's information. The user gets to decide if this is OK or not. If the user decides that this is not OK, the dance is over. 4. If the user grants consent, the provider makes up a new secret, called the "authorization code". The provider crafts a special URL that points to the consumer, but has the authorization code and the state token embedded in it. The provider asks the client to visit the consumer using this special URL. 5. When the client visits the consumer at that URL, the consumer first checks the state token to be sure that it hasn't changed, just to verify that no one has tampered with the request. Then, the consumer makes a separate request to the provider, passing along the client secret and the authorization code. If everything looks good to the provider, the provider makes up one final secret, called the "access token", and sends it back to the consumer. This completes the dance. OAuth 1 ------- .. seqdiag:: seqdiag { Client -> Consumer [label = "start dance"]; Consumer --> Provider [label = "send client secret"]; Consumer <-- Provider [label = "return request token"]; Client <- Consumer [label = "redirect, with request token"]; Client -> Provider [label = "follow redirect", rightnote="user\ngrants\nconsent"]; Client <- Provider [label = "redirect, with authorization code"]; Client -> Consumer [label = "follow redirect"]; Consumer --> Provider [label = "send client secret\n& authorization code"]; Consumer <-- Provider [label = "return access token"]; Client <-- Consumer [label = "render page or redirect"]; } 1. The client visits the consumer at a special URL, indicating that they want to connect to the provider with OAuth. Typically, there is a button on the consumer's website labeled "Log In with Google" or similar, which takes the user to this special URL. 2. The consumer tells the provider that they're about to do the OAuth dance. The consumer gives the provider the client secret, to verify that everything's cool. The provider checks the OAuth secret, and if it looks good, the provider makes up a new secret called a "request token", and gives it to the consumer. 3. The consumer crafts a special URL that points to the provider, but has the client secret and request token embedded in it. The consumer asks the client to visit the provider using this special URL. 4. When the client visits the provider at that URL, the provider notices the request token, and looks up the consumer that it belongs to. The provider tells the user that this consumer wants to access some or all of the user's information. The user gets to decide if this is OK or not. If the user decides that this is not OK, the dance is over. 5. If the user grants consent, the provider makes up another new secret, called the "authorization code". The provider crafts a special URL that points to the consumer, but has the authorization code embedded in it. The provider asks the client to go visit the consumer at that special URL. 6. When the client visits the consumer at that URL, the consumer notices the authorization code. The consumer makes another request to the provider, passing along the client secret and the authorization code. If everything looks good to the provider, the provider makes up one final secret, called the "access token", and sends it back to the consumer. This completes the dance. Dance Complete -------------- Phew, that was complicated! But the end result is, the consumer has an access token, which proves that the user has given consent for the provider to give the consumer information about that user. Now, whenever the consumer needs information from the provider about the user, the consumer simply makes an API request to the provider and passes the access token along with the request. The provider sees the access token, looks up the user that granted consent, and determines whether the requested information falls within what the user authorized. If so, the provider returns that information to the consumer. In effect, the consumer is now the user's client! .. warning:: Keep your access tokens secure! Treat a user's access token like you would treat their password. .. note:: The OAuth dance normally only needs to be performed once per user. Once the consumer has an access token, that access token can be used to make many API requests on behalf of the user. Some OAuth implementations put a lifespan on the access token, after which it must be refreshed, but refreshing an access token does not require any interaction from the user. .. [#oauth-user] Not all OAuth interactions share information about specific users. When no user-specific information is involved, then the consumer is able to get information from the provider without getting a user's consent, since there is no one to get consent from. In practice, however, most OAuth interactions are about sharing information about users, so this documentation assumes that use-case. flask-dance-7.1.0/docs/index.rst000066400000000000000000000027171457161140100164670ustar00rootroot00000000000000Flask-Dance =========== Doing the OAuth dance with style using `Flask`_, `requests`_, and `oauthlib`_. Check out just how easy it can be to hook up your Flask app with OAuth: .. code-block:: python from flask import Flask, redirect, url_for from flask_dance.contrib.github import make_github_blueprint, github app = Flask(__name__) app.secret_key = "supersekrit" # Replace this with your own secret! blueprint = make_github_blueprint( client_id="my-key-here", client_secret="my-secret-here", ) app.register_blueprint(blueprint, url_prefix="/login") @app.route("/") def index(): if not github.authorized: return redirect(url_for("github.login")) resp = github.get("/user") assert resp.ok return "You are @{login} on GitHub".format(login=resp.json()["login"]) Ready to get started? .. _Flask: http://flask.pocoo.org/ .. _requests: https://requests.readthedocs.io/ .. _oauthlib: https://oauthlib.readthedocs.io/ User Guide ---------- .. toctree:: :maxdepth: 1 install quickstart concepts understanding-the-magic multi-user logout examples Options & Configuration ----------------------- .. toctree:: :maxdepth: 2 providers storages signals Advanced Topics --------------- .. toctree:: how-oauth-works proxies testing api contributing Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` flask-dance-7.1.0/docs/install.rst000066400000000000000000000005321457161140100170170ustar00rootroot00000000000000Installation ============ Use `pip`_ to install Flask-Dance. To use basic functionality, run this: .. code-block:: bash $ pip install Flask-Dance To also use the :ref:`SQLAlchemy storage `, specify the ``sqla`` extra, like this: .. code-block:: bash $ pip install Flask-Dance[sqla] .. _pip: https://pip.pypa.io flask-dance-7.1.0/docs/logout.rst000066400000000000000000000157111457161140100166670ustar00rootroot00000000000000Logging Out =========== Many websites use OAuth as an authentication system (see :doc:`multi-user` for more information). Although this can work quite well, it gets more complicated when you want to allow the user to log out of your website. For starters, we need to understand what "logging out" even *means*. Local Accounts vs Provider Accounts ----------------------------------- When you use OAuth as an authentication system, your users still have local accounts on your system. OAuth allows your users to log in with a provider (such as Google or Facebook), and use their provider login to authenticate to your local account. Essentially, the exchange looks something like this, if we assume that "CustomSite" is the name of your website, and it's using the Google provider: 1. User: Hey CustomSite, I'm user ShinyStar99. Let me in. 2. CustomSite: Sorry user, anyone could claim that and I don't trust you. How do I know you're telling the truth? 3. User: My friend Google can back me up. You trust Google, right? 4. CustomSite: Yes, I trust Google. Hey Google, who is this user? 5. Google: Hold on, I need to ask my user if I have permission to give you any information at all. Hey User, CustomSite wants to know who you are. Do you want me to tell CustomSite some basic information about you? 6. User: Yes, please. 7. Google: Alright CustomSite, I can tell you that on *my* website, this user has the ID 987654. 8. CustomSite: Alright, let me check my database. Google user ID 987654 matches up with one of my local users, with ID 12345. And it looks like that local user is ShinyStar99! 9. User: You see? I told you so! 10. CustomSite: Come in, ShinyStar99. Who's next? In this exchange, you can see that there are two *different* user accounts involved: one user account on Google, and one user account on CustomSite. They have different user IDs, and could contain different sets of information, even though they both represent the same user. So if you want to cause a user to log out, what exactly do you mean? We'll go step-by-step through the different options. Log Out Local Account --------------------- If you want to cause a user to log out of their local user account, check the documentation for whatever system you're using to manage local accounts. If you're using `Flask-Login`_ (or `Flask-Security`_, which is built on top of Flask-Login), you can import and call the :func:`flask_login.logout_user` function, like this: .. code-block:: python :emphasize-lines: 6 from flask_login import logout_user # other imports as necessary @app.route("/logout") def logout(): logout_user() return redirect(somewhere) After you do this, your application will treat the user like any other anonymous user. However, logging out your user from their local account doesn't do anything about the provider account. As a result, if the user tries to log in again after you've logged them out this way, the conversation will look like this: 1. User: Hey CustomSite, I'm user ShinyStar99. Let me in. Ask Google if you don't believe me. 2. CustomSite: Hey Google, who is this user? 3. Google: My user already gave me permission to tell you some basic information. On *my* website, this user has the ID 987654. 4. CustomSite: Oh right, it's ShinyStar99 again. Go ahead. In most cases, this is what you want. However, sometimes you want to really reset things back to the start, as though the user had never granted consent to share information in the first place. Revoking Access with the Provider --------------------------------- Undoing the user's permission to share information from the provider to your custom site is called "revoking access". Unfortunately, every provider has a different way of doing this, so you'll need to check the OAuth documentation provided by your OAuth provider. When you are granted access by a user, the provider will give your application a "token" that is used for making subsequent API requests. In order to revoke access for a user, you may need to include this token as an argument, so the provider knows which token to revoke. You can get this information by checking the ``token`` property of the Flask-Dance blueprint. We'll use Google as an example. First, check `Google's documentation for how to revoke access via OAuth2 `_. Notice that you do indeed need to provide the token in order to revoke it. Here's some sample code that works with Google: .. code-block:: python :emphasize-lines: 12-16 from flask import Flask, redirect from flask_dance.contrib.google import make_google_blueprint, google from flask_login import logout_user app = Flask(__name__) blueprint = make_google_blueprint() app.register_blueprint(blueprint, url_prefix="/login") @app.route("/logout") def logout(): token = blueprint.token["access_token"] resp = google.post( "https://accounts.google.com/o/oauth2/revoke", params={"token": token}, headers={"Content-Type": "application/x-www-form-urlencoded"} ) assert resp.ok, resp.text logout_user() # Delete Flask-Login's session cookie del blueprint.token # Delete OAuth token from storage return redirect(somewhere) After the user uses this method to log out, Google will not remember that they granted consent to share information with your website. .. warning:: In this sample code, we are using an :keyword:`assert` statement. This works fine for debugging, but not for production. Be sure to modify this code to appropriately handle cases where there is an API failure when trying to revoke the token. .. note:: In this code, we already have a reference to the ``blueprint`` object, so we could grab the token easily. But what if you don't have access to that object? Instead, you can use the :data:`flask.current_app` proxy to pull out the blueprint object you need. For example, instead of this line: .. code-block:: python token = blueprint.token["access_token"] You could use this line instead: .. code-block:: python token = current_app.blueprints["google"].token["access_token"] Log Out Provider Account ------------------------ You can log out the user from their local account, and you can revoke access with the provider. But what about logging the user out from their provider account? Can you force the user to type their password into Google again if they want to log in to your website in the future? The short answer is: no, you can't. You can't control how a user interacts with other websites, except for in the ways that those other websites specifically allow you to. And since this could potentially be used as part of a security exploit, websites will generally *not* allow you to force users to log out. .. _Flask-Login: https://flask-login.readthedocs.io/ .. _Flask-Security: https://pythonhosted.org/Flask-Security/ flask-dance-7.1.0/docs/make.bat000066400000000000000000000150651457161140100162330ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\FlaskDance.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\FlaskDance.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end flask-dance-7.1.0/docs/multi-user.rst000066400000000000000000000236111457161140100174620ustar00rootroot00000000000000Multi-User Setups ================= Many websites are designed to have multiple user accounts, where each user has one or more OAuth connections to other websites, like Google or Twitter. This is a perfectly valid use-case for OAuth, but in order to implement it correctly, you need to think carefully about how these OAuth connections are created and used. There are a lot of unexpected edge-cases that can take you by surprise. Defining Expected Behavior -------------------------- User Association vs User Creation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Are users expected to create an account on your website *first*, and then associate OAuth connections *afterwards*? Or does logging in with an an OAuth provider *create an account* for the user on your site automatically? The first option (user association) is useful when you expect users to primarily log in to your website using a username/password combination, but want to allow your users to perform actions on other sites via OAuth. For example, maybe you want to build your own social network website, and allow users to invite their friends from Facebook and their followers on Twitter. Typically, this setup means that users are able to associate their accounts with other websites via OAuth, but they are not required to do so. The second option (user creation) is useful when you expect users to primarily (or exclusively) log in to your website using an OAuth connection. For example, maybe you don't want your users to have to remember another username/password combination, so instead, you have a "Log In with Google" or "Log In with GitHub" button on your website. When a user clicks on that button and logs in with the respective service, they automatically create an account on your website in the process. Typically, this setup means that users cannot create an account on your website without associating it with an OAuth connection. Associations with Multiple Providers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Can a user associate one account with multiple different OAuth providers? For example, can a user login with Google *or* login with GitHub, and log into the same account whichever option they pick? This is particularly complicated if you've chosen user creation via OAuth, instead of user association. When a user logs in with a provider, and your website hasn't seen that particular user on that particular provider before, how does your website know whether to create a new user on your website, or link this provider to an existing user on your website? If you use user association, you can simply require that the user should already be logged in to their local account before they can associate that local account with an OAuth provider. But if you use user creation, that requirement is almost impossible to enforce, because typically people don't understand that they *have* a local user account. Flask-Dance's Default Behavior ------------------------------ Flask-Dance does the best it can to resolve these issues for you, while allowing you to take control in complex circumstances. Different token storages may handle this differently, but for simplicity, this document will refer to the :ref:`SQLAlchemy storage `. User Association vs User Creation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Dance will *never* create user accounts for your users automatically. Flask-Dance *only* handles creating OAuth associations and retrieving them on a per-user basis. By default, Flask-Dance will associate new OAuth connections with the local user that is currently logged in. What happens if there no local user is currently logged in? That depends on the ``user_required`` parameter of the :class:`~flask_dance.consumer.storage.sqla.SQLAlchemyStorage` class. If it is ``False``, Flask-Dance will create an association that isn't linked to any particular user in your application. This is handy if you don't actually *have* local user accounts in your application, and are using Flask-Dance to connect your entire website to one single remote user. For example, this could be the desired behavior if your website is actually a bot that responds to incoming requests by making API calls to a third-party website, like a Twitter bot that tweets in response to certain HTTP requests. If the ``user_required`` parameter is set to ``True``, and no local user is currently logged in, then Flask-Dance will raise an exception when trying to associate an OAuth connection with the local user. The only way to correctly resolve this situation is to override Flask-Dance's default behavior and specify exactly how to create a local user. Associations with Multiple Providers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, Flask-Dance will happily associate multiple different OAuth providers with a single user account. This is why the ``OAuth`` model in SQLAlchemy must be separate from the ``User`` model: so that you can associate multiple different ``OAuth`` models with a single ``User`` model. Since Flask-Dance does user association by default, rather than user creation, you don't need to worry about the question of how Flask-Dance will handle new OAuth associations. Using the default behavior, Flask-Dance will *never* create a new user for the connection; instead, it will *always* associate the connection with an existing user. Overriding the Default Behavior ------------------------------- If you want to allow users to log in with OAuth, and create local user accounts automatically when they do so, you'll need to override Flask-Dance's default behavior. To do so, you'll need to hook into the :data:`~flask_dance.consumer.oauth_authorized` signal. Flask-Dance's default behavior comes from storing the OAuth token for you automatically. To override the default behavior, write a function that subscribes to this signal, handles it the way *you* want, and returns ``False`` or a :class:`~werkzeug.wrappers.Response` object. Returning ``False`` or a :class:`~werkzeug.wrappers.Response` object from this signal handler indicates to Flask-Dance that it should not try to store the OAuth token for you. For example, returning a custom redirect like :func:`flask.redirect` would override the default behavior. .. warning:: If you return ``False`` from a :data:`~flask_dance.consumer.oauth_authorized` signal handler, and you do *not* store the OAuth token in your database, the OAuth token will be lost, and you will not be able to use it to make API calls in the future! Here's an example of how you might want to override Flask-Dance's default behavior in order to create user accounts automatically: .. code-block:: python import flask from flask import flash from flask_security import current_user, login_user from flask_dance.consumer import oauth_authorized from flask_dance.consumer.storage.sqla import SQLAlchemyStorage from flask_dance.contrib.github import make_github_blueprint from sqlalchemy.orm.exc import NoResultFound from myapp.models import db, OAuth, User github_bp = make_github_blueprint( storage=SQLAlchemyStorage(OAuth, db.session, user=current_user) ) # create/login local user on successful OAuth login @oauth_authorized.connect_via(github_bp) def github_logged_in(blueprint, token): if not token: flash("Failed to log in with GitHub.", category="error") return False resp = blueprint.session.get("/user") if not resp.ok: msg = "Failed to fetch user info from GitHub." flash(msg, category="error") return False github_info = resp.json() github_user_id = str(github_info["id"]) # Find this OAuth token in the database, or create it query = OAuth.query.filter_by( provider=blueprint.name, provider_user_id=github_user_id, ) try: oauth = query.one() except NoResultFound: oauth = OAuth( provider=blueprint.name, provider_user_id=github_user_id, token=token, ) if oauth.user: # If this OAuth token already has an associated local account, # log in that local user account. # Note that if we just created this OAuth token, then it can't # have an associated local account yet. login_user(oauth.user) flash("Successfully signed in with GitHub.") else: # If this OAuth token doesn't have an associated local account, # create a new local user account for this user. We can log # in that account as well, while we're at it. user = User( # Remember that `email` can be None, if the user declines # to publish their email address on GitHub! email=github_info["email"], name=github_info["name"], ) # Associate the new local user account with the OAuth token oauth.user = user # Save and commit our database models db.session.add_all([user, oauth]) db.session.commit() # Log in the new local user account login_user(user) flash("Successfully signed in with GitHub.") # Since we're manually creating the OAuth model in the database, # we should return False so that Flask-Dance knows that # it doesn't have to do it. If we don't return False, the OAuth token # could be saved twice, or Flask-Dance could throw an error when # trying to incorrectly save it for us. return False This example code does not include implementations for the ``User`` and ``OAuth`` models: you can see that these models are imported from another file. However, notice that the ``OAuth`` model has a field called ``provider_user_id``, which is used to store the user ID of the GitHub user. The example code uses that ID to check if we've already saved an OAuth token in the database for this GitHub user. flask-dance-7.1.0/docs/providers.rst000066400000000000000000000253401457161140100173720ustar00rootroot00000000000000Providers ========= Flask-Dance comes with pre-set OAuth consumer configurations for a few popular OAuth providers. Flask-Dance also works with providers that aren't in this list: see the :ref:`Custom ` section at the bottom of the page. We also welcome pull requests to add new pre-set provider configurations to Flask-Dance! .. contents:: Included Providers :local: :backlinks: none Atlassian --------- .. module:: flask_dance.contrib.atlassian .. autofunction:: make_atlassian_blueprint .. data:: atlassian A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Atlassian authentication token loaded (assuming that the user has authenticated with Atlassian at some point in the past). Authentiq --------- .. module:: flask_dance.contrib.authentiq .. autofunction:: make_authentiq_blueprint .. data:: authentiq A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Authentiq Connect authentication token loaded (assuming that the user has authenticated with Authentiq at some point in the past). Azure ----- .. module:: flask_dance.contrib.azure .. autofunction:: make_azure_blueprint .. data:: azure A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Azure AD authentication token loaded (assuming that the user has authenticated with Azure AD at some point in the past). Dexcom ------ .. module:: flask_dance.contrib.dexcom .. autofunction:: make_dexcom_blueprint .. data:: dexcom A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Dexcom authentication token loaded (assuming that the user has authenticated with Dexcom at some point in the past). Digital Ocean ------------- .. module:: flask_dance.contrib.digitalocean .. autofunction:: make_digitalocean_blueprint .. data:: digitalocean A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Digital Ocean authentication token loaded (assuming that the user has authenticated with Digital Ocean at some point in the past). Discord ------- .. module:: flask_dance.contrib.discord .. autofunction:: make_discord_blueprint .. data:: discord A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Discord authentication token loaded (assuming that the user has authenticated with Discord at some point in the past). Dropbox ------- .. module:: flask_dance.contrib.dropbox .. autofunction:: make_dropbox_blueprint .. data:: dropbox A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Dropbox authentication token loaded (assuming that the user has authenticated with Dropbox at some point in the past). Facebook -------- .. module:: flask_dance.contrib.facebook .. autofunction:: make_facebook_blueprint .. data:: facebook A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Facebook authentication token loaded (assuming that the user has authenticated with Facebook at some point in the past). Fitbit ------ .. module:: flask_dance.contrib.fitbit .. autofunction:: make_fitbit_blueprint .. data:: fitbit A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Fitbit authentication token loaded (assuming that the user has authenticated with Fitbit at some point in the past). GitHub ------ .. module:: flask_dance.contrib.github .. autofunction:: make_github_blueprint .. data:: github A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the GitHub authentication token loaded (assuming that the user has authenticated with GitHub at some point in the past). GitLab ------ .. module:: flask_dance.contrib.gitlab .. autofunction:: make_gitlab_blueprint .. data:: gitlab A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the GitLab authentication token loaded (assuming that the user has authenticated with GitLab at some point in the past). Google ------ .. module:: flask_dance.contrib.google .. autofunction:: make_google_blueprint .. data:: google A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Google authentication token loaded (assuming that the user has authenticated with Google at some point in the past). Heroku ------ .. module:: flask_dance.contrib.heroku .. autofunction:: make_heroku_blueprint .. data:: heroku A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Heroku authentication token loaded (assuming that the user has authenticated with Heroku at some point in the past). JIRA ---- .. module:: flask_dance.contrib.jira .. autofunction:: make_jira_blueprint .. data:: jira A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the JIRA authentication token loaded (assuming that the user has authenticated with JIRA at some point in the past). LinkedIn -------- .. module:: flask_dance.contrib.linkedin .. autofunction:: make_linkedin_blueprint .. data:: linkedin A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the LinkedIn authentication token loaded (assuming that the user has authenticated with LinkedIn at some point in the past). Meetup ------ .. module:: flask_dance.contrib.meetup .. autofunction:: make_meetup_blueprint .. data:: meetup A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Meetup authentication token loaded (assuming that the user has authenticated with Meetup at some point in the past). Nylas ----- .. module:: flask_dance.contrib.nylas .. autofunction:: make_nylas_blueprint .. data:: nylas A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Nylas authentication token loaded (assuming that the user has authenticated with Nylas at some point in the past). OpenStreetMap ------------- .. module:: flask_dance.contrib.osm .. autofunction:: make_osm_blueprint .. data:: osm A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the OpenStreetMap authentication token loaded (assuming that the user has authenticated with OpenStreetMap at some point in the past). ORCID ------------- .. module:: flask_dance.contrib.orcid .. autofunction:: make_orcid_blueprint .. data:: orcid A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the ORCID (or sandbox ORCID) authentication token loaded (assuming that the user has authenticated with ORCID at some point in the past). Reddit ------ .. module:: flask_dance.contrib.reddit .. autofunction:: make_reddit_blueprint .. data:: reddit A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Reddit authentication token loaded (assuming that the user has authenticated with Reddit at some point in the past). Salesforce ---------- .. module:: flask_dance.contrib.salesforce .. autofunction:: make_salesforce_blueprint .. data:: salesforce A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Salesforce authentication token loaded (assuming that the user has authenticated with Salesforce at some point in the past). Slack ----- .. module:: flask_dance.contrib.slack .. autofunction:: make_slack_blueprint .. data:: slack A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Slack authentication token loaded (assuming that the user has authenticated with Slack at some point in the past). Strava ------ .. module:: flask_dance.contrib.strava .. autofunction:: make_strava_blueprint .. data:: strava A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Strava authentication token loaded (assuming that the user has authenticated with Strava at some point in the past). Twitch ------- .. module:: flask_dance.contrib.twitch .. autofunction:: make_twitch_blueprint .. data:: twitch A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Twitch authentication token loaded (assuming that the user has authenticated with Twitch at some point in the past). Spotify ------- .. module:: flask_dance.contrib.spotify .. autofunction:: make_spotify_blueprint .. data:: spotify A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Spotify authentication token loaded (assuming that the user has authenticated with Spotify at some point in the past). Zoho ---- .. module:: flask_dance.contrib.zoho .. autofunction:: make_zoho_blueprint .. data:: zoho A :class:`~werkzeug.local.LocalProxy` to a :class:`requests.Session` that already has the Zoho authentication token loaded (assuming that the user has authenticated with Zoho at some point in the past). .. _custom-provider: Custom ------ Flask-Dance allows you to build authentication blueprints for any OAuth provider, not just the ones listed above. For example, let's create a blueprint for a fictional OAuth provider called oauth-example.com. We check the documentation for oauth-example.com, and discover that they're using OAuth 2, the access token URL is ``https://oauth-example.com/login/access_token``, and the authorization URL is ``https://oauth-example.com/login/authorize``. We could then build the blueprint like this: .. code-block:: python from flask import Flask from flask_dance.consumer import OAuth2ConsumerBlueprint app = Flask(__name__) example_blueprint = OAuth2ConsumerBlueprint( "oauth-example", __name__, client_id="my-key-here", client_secret="my-secret-here", base_url="https://oauth-example.com", token_url="https://oauth-example.com/login/access_token", authorization_url="https://oauth-example.com/login/authorize", ) app.register_blueprint(example_blueprint, url_prefix="/login") Now, in your page template, you can do something like: .. code-block:: jinja Login with OAuth Example And in your views, you can make authenticated requests using the :attr:`~flask_dance.consumer.OAuth2ConsumerBlueprint.session` attribute on the blueprint: .. code-block:: python resp = example_blueprint.session.get("/user") assert resp.ok print("Here's the content of my response: " + resp.content) It all follows the same patterns as the :doc:`quickstart` example projects. You can also read the code to see how the pre-set configurations are implemented -- it's very short. flask-dance-7.1.0/docs/proxies.rst000066400000000000000000000042571457161140100170520ustar00rootroot00000000000000Proxies and HTTPS ================= Running a secure HTTPS website is important, but encrypting and decrypting HTTPS traffic is computationally expensive. Many people running large-scale websites (including `Heroku`_) use a `TLS termination proxy`_ to reduce load on the HTTP server. This works great, but means that the webserver running your Flask application is actually speaking HTTP, not HTTPS. As a result, Flask-Dance can get confused, and generate callback URLs that have an ``http://`` scheme, instead of an ``https://`` scheme. This is bad, because OAuth requires that all connections use HTTPS for security purposes, and OAuth providers will reject requests that suggest a callback URL with a ``http://`` scheme. When you proxy the request from a `TLS termination proxy`_, probably your load balancer, you need to ensure a few headers are set/proxied correctly for Flask to do the right thing out of the box: * ``Host``: preserve the Host header of the original request * ``X-Real-IP``: preserve the source IP of the original request * ``X-Forwarded-For``: a list of IP addresses of the source IP and any HTTP proxies we've been through * ``X-Forwarded-Proto``: the protocol, http or https, that the request came in with In 99.9% of the cases the `TLS termination proxy`_ will be configured to do the right thing by default and any well-behaved Flask application will work out of the box. However, if you're accessing the WSGI environment directly, you will run into trouble. Don't do this and instead use the functions provided by Werkzeug's :mod:`~werkzeug.wsgi` module or Flask's :attr:`~flask.request` to access things like a ``Host`` header. If your Flask app is behind a TLS termination proxy, and you need to make sure that Flask is aware of that, check Flask's documentation for :external:doc:`how to deploy a proxy setup `. Please read it and follow its instructions. This is not unique to Flask-Dance and there's nothing to configure on Flask-Dance's side to solve this. It's also worth noting you might wish to set Flask's :data:`PREFERRED_URL_SCHEME`. .. _TLS termination proxy: https://en.wikipedia.org/wiki/TLS_termination_proxy .. _Heroku: https://www.heroku.com/ flask-dance-7.1.0/docs/quickstart.rst000066400000000000000000000045051457161140100175470ustar00rootroot00000000000000Quickstart ========== The fastest way to get up and running with Flask-Dance is to start from an example project. First, decide which :doc:`token storage ` you want to use: * :class:`~flask_dance.consumer.storage.session.SessionStorage` is the default because it requires zero configuration. It uses the :ref:`Flask session ` to store OAuth tokens. It's the easiest for getting started, but it's not a good choice for production applications. * :class:`~flask_dance.consumer.storage.sqla.SQLAlchemyStorage` uses a relational database to store OAuth tokens. It's great for production usage, but it requires a relational database with `SQLAlchemy`_ and it's more complicated to set up. If you're not sure which to pick, start with ``SessionStorage``. You can switch later, if you want. Next, check the lists below to find the OAuth provider you're interested in and jump to an example project that uses Flask-Dance with that provider! Flask sessions (easiest) ------------------------ * `GitHub `__ * `Google `__ * `Facebook `__ * `Slack `__ * `LinkedIn `__ * `Heroku `__ * `Fitbit `__ SQLAlchemy ---------- * `Google `__ * `Google with Flask-Security `__ * `Facebook `__ * `Heroku `__ * `Multiple providers simultaneously `__ .. admonition:: Other Providers Don't see the OAuth provider you want? Flask-Dance provides :doc:`built-in support for even more providers `, and you can configure Flask-Dance to support :ref:`any custom provider you want `. Start with any of the example projects listed above, and modify it to use the provider you want! .. _SQLAlchemy: http://www.sqlalchemy.org/ flask-dance-7.1.0/docs/requirements.txt000066400000000000000000000002301457161140100200760ustar00rootroot00000000000000sphinx>=1.3 sphinxcontrib-seqdiag sphinxcontrib-spelling Flask-Sphinx-Themes # code dependencies, needed for imports sqlalchemy>=1.3.11 pytest betamax flask-dance-7.1.0/docs/signals.rst000066400000000000000000000075351457161140100170230ustar00rootroot00000000000000.. module:: flask_dance.consumer Signals ======= Flask-Dance supports signals, :doc:`just as Flask does `. Signals are perfect for custom processing code that you want to run at a certain point in the OAuth dance. For example, after the dance is complete, you might need to update the user's profile, kick off a long-running task, or simply :doc:`flash a message ` to let the user know that the login was successful. It's easy, just import the appropriate signal of the ones listed below, and connect your custom processing code to the signal. The following signals exist in Flask-Dance: .. data:: oauth_before_login .. versionadded:: 1.4.0 This signal is sent before redirecting to the provider login page. The signal is sent with a ``url`` parameter specifying the redirect URL. This signal is mostly useful for doing things like session construction/deconstruction before the user is redirected. Example subscriber:: import flask from flask_dance.consumer import oauth_before_login @oauth_before_login.connect def before_login(blueprint, url): flask.session["next_url"] = flask.request.args.get("next_url") .. data:: oauth_authorized This signal is sent when a user completes the OAuth dance by receiving a response from the OAuth provider's authorize URL. The signal is invoked with the blueprint instance as the first argument (the *sender*), and with a dict of the OAuth provider's response (the *token*). Example subscriber:: from flask import flash from flask_dance.consumer import oauth_authorized @oauth_authorized.connect def logged_in(blueprint, token): flash("Signed in successfully with {name}!".format( name=blueprint.name.capitalize() )) If you are linking OAuth records to User records, you *must* implement an ``@oauth_authorized`` subscriber that creates new ``User`` and ``OAuth`` database entries for any new users, and links those two new records via the ``OAuth`` table's ``user_id`` field. If you're using OAuth 2, the user may grant you different scopes from the ones you requested: check the ``scope`` key in the *token* dict to determine what scopes were actually granted. If you don't want the *token* to be :doc:`stored `, simply return ``False`` from one of your signal receiver functions -- this can be useful if the user has declined to authorize your OAuth request, has granted insufficient scopes, or in some other way has given you a token that you don't want. You can also return a :class:`~flask.Response` instance from an event subscriber. If you do, that response will be returned to the user instead of the normal redirect. For example:: from flask import redirect, url_for @oauth_authorized.connect def logged_in(blueprint, token): return redirect(url_for("after_oauth")) .. data:: oauth_error This signal is sent when the OAuth provider indicates that there was an error with the OAuth dance. This can happen if your application is misconfigured somehow. The user will be redirected to the ``redirect_url`` anyway, so it is your responsibility to hook into this signal and inform the user that there was an error. You can also return a :class:`~flask.Response` instance from an event subscriber. If you do, that response will be returned to the user instead of the normal redirect. For example:: from flask import redirect, url_for @oauth_error.connect def handle_error(blueprint, error, error_description=None, error_uri=None): return redirect(url_for("custom_error_page")) .. _flash a message: http://flask.pocoo.org/docs/latest/patterns/flashing/ .. _blinker: http://pythonhosted.org/blinker/ flask-dance-7.1.0/docs/spelling_wordlist.txt000066400000000000000000000010451457161140100211240ustar00rootroot00000000000000OAuth oauth app apps config dotpath backend username subdomain changelog precendence openedx webhooks webserver http https url hostname indices pre py sqla quickstart misconfigured performant contrib proxied storages superclass decrypting balancer initializer Authentiq authentiq Azure Discord Dropbox Dexcom DigitalOcean Facebook GitHub GitLab Google Heroku Jira LinkedIn Meetup Nylas OpenStreetMap Okta Reddit Slack Spotify Strava Twitter Zoho Atlassian Fitbit Pytest Werkzeug Betamax CustomSite # contractions are buggy isn aren hasn doesn ve flask-dance-7.1.0/docs/storages.rst000066400000000000000000000106501457161140100172020ustar00rootroot00000000000000.. module:: flask_dance.consumer.storage Token Storages ============== A Flask-Dance blueprint has a token storage associated with it, which is an object that knows how to store and retrieve OAuth tokens from some kind of persistent storage. A storage is most often some kind of database, but it doesn't have to be. .. _flask-session-storage: Flask Session ------------- The default token storage uses the :ref:`Flask session ` to store OAuth tokens, which is simple and requires no configuration. However, when the user closes their browser, their OAuth token will be lost, so its not a good choice for production usage. This is a great option for hobby projects, and for a "proof of concept" to show that an idea is viable. .. _sqlalchemy-storage: SQLAlchemy ---------- SQLAlchemy is the "standard" ORM_ for Flask applications, and Flask-Dance has great support for it. First, define your database model with a ``token`` column and a ``provider`` column. Flask-Dance includes a :class:`~flask_dance.consumer.storage.sqla.OAuthConsumerMixin` class to make this easier:: from flask_sqlalchemy import SQLAlchemy from flask_dance.consumer.storage.sqla import OAuthConsumerMixin db = SQLAlchemy() class OAuth(OAuthConsumerMixin, db.Model): pass Next, create an instance of the SQLAlchemy storage and assign it to your blueprint:: from flask_dance.consumer.storage.sqla import SQLAlchemyStorage blueprint.storage = SQLAlchemyStorage(OAuth, db.session) And that's all you need -- if you don't have user accounts in your application. If you do, it's slightly more complicated:: from flask_sqlalchemy import SQLAlchemy from flask_login import current_user from flask_dance.consumer.storage.sqla import OAuthConsumerMixin, SQLAlchemyStorage db = SQLAlchemy() class User(db.Model): id = db.Column(db.Integer, primary_key=True) # ... other columns as needed class OAuth(OAuthConsumerMixin, db.Model): user_id = db.Column(db.Integer, db.ForeignKey(User.id)) user = db.relationship(User) blueprint.storage = SQLAlchemyStorage(OAuth, db.session, user=current_user) There are two things to notice here. One, the model that you use for storing OAuth tokens must have a :attr:`user` relationship to the user that it is associated with. Two, you must pass a reference to the currently logged-in user (if any) to :class:`~flask_dance.consumer.storage.sqla.SQLAlchemyStorage`. If you're using `Flask-Login`_, the :attr:`current_user` proxy works great, but you could instead pass a function that returns the current user, if you want. You also probably want to use a caching system for your database, so that it is more performant under heavy load. The SQLAlchemy token storage also integrates with `Flask-Caching`_ if you pass an instance of Flask-Caching to the storage, like this:: from flask import Flask from flask_caching import Cache app = Flask(__name__) cache = Cache(app) # setup Flask-Dance with SQLAlchemy models... blueprint.storage = SQLAlchemyStorage(OAuth, db.session, cache=cache) .. _SQLAlchemy: http://www.sqlalchemy.org/ .. _Flask-Login: https://flask-login.readthedocs.io/ .. _Flask-Caching: https://flask-caching.readthedocs.io/ Custom ------ Of course, you don't have to use `SQLAlchemy`_, you're free to use whatever storage system you want. Writing a custom token storage is easy: just subclass :class:`flask_dance.consumer.storage.BaseStorage` and override the :meth:`get`, :meth:`set`, and :meth:`delete` methods. For example, here's a storage that uses a file on disk:: import os import os.path import json from flask_dance.consumer.storage import BaseStorage class FileStorage(BaseStorage): def __init__(self, filepath): super(FileStorage, self).__init__() self.filepath = filepath def get(self, blueprint): if not os.path.exists(self.filepath): return None with open(self.filepath) as f: return json.load(f) def set(self, blueprint, token): with open(self.filepath, "w") as f: json.dump(token, f) def delete(self, blueprint): os.remove(self.filepath) Then, just create an instance of your storage and assign it to the :attr:`storage` attribute of your blueprint, and Flask-Dance will use it. .. _ORM: https://docs.python.org/3.4/howto/webservers.html#data-persistence flask-dance-7.1.0/docs/testing.rst000066400000000000000000000172541457161140100170370ustar00rootroot00000000000000Testing Apps That Use Flask-Dance ================================= Automated tests are a great way to keep your Flask app stable and working smoothly. The Flask documentation has :doc:`some great information on how to write automated tests for Flask apps `. However, Flask-Dance presents some challenges for writing tests. What happens when you have a view function that requires OAuth authorization? How do you handle cases where the user has a valid OAuth token, an expired token, or no token at all? Fortunately, we've got you covered. Mock Storages ------------- The simplest way to write tests with Flask-Dance is to use a mock token storage. This allows you to easily control whether Flask-Dance believes the current user is authorized with the OAuth provider or not. Flask-Dance provides two mock token storages: .. currentmodule:: flask_dance.consumer.storage .. autoclass:: NullStorage .. autoclass:: MemoryStorage Let's say you are testing the following code:: from flask import redirect, url_for from flask_dance.contrib.github import make_github_blueprint, github app = Flask(__name__) github_bp = make_github_blueprint() app.register_blueprint(github_bp, url_prefix="/login") @app.route("/") def index(): if not github.authorized: return redirect(url_for("github.login")) return "You are authorized" You want to write tests to cover two cases: what happens when the user is authorized with the OAuth provider, and what happens when they are not. Here's how you could do that with `pytest`_ and the :class:`MemoryStorage`: .. code-block:: python :emphasize-lines: 6, 16 from flask_dance.consumer.storage import MemoryStorage from myapp import app, github_bp def test_index_unauthorized(monkeypatch): storage = MemoryStorage() monkeypatch.setattr(github_bp, "storage", storage) with app.test_client() as client: response = client.get("/", base_url="https://example.com") assert response.status_code == 302 assert response.headers["Location"] == "https://example.com/login/github" def test_index_authorized(monkeypatch): storage = MemoryStorage({"access_token": "fake-token"}) monkeypatch.setattr(github_bp, "storage", storage) with app.test_client() as client: response = client.get("/", base_url="https://example.com") assert response.status_code == 200 text = response.get_data(as_text=True) assert text == "You are authorized" In this example, we're using the `monkeypatch fixture `__ to set a mock storage on the Flask-Dance blueprint. This fixture will ensure that the original storage is put back on the blueprint after the test is finished, so that the test doesn't change the code being tested. Then, we create a test client and access the ``index`` view. The mock storage will control whether ``github.authorized`` is ``True`` or ``False``, and the rest of the test asserts that the result is what we expect. Mock API Responses ------------------ Once you've gotten past the question of whether the current user is authorized or not, you still have to account for any API calls that your view makes. It's usually a bad idea to make real API calls in an automated test: not only does it make your tests run significantly more slowly, but external factors like rate limits can affect whether your tests pass or fail. There are several other libraries that you can use to mock API responses, but I recommend Betamax_. It's powerful, flexible, and it's designed to work with Requests_, the HTTP library that Flask-Dance is built on. Betamax is also created and maintained by one of the primary maintainers of the Requests library, `@sigmavirus24`_. Let's say your testing the same code as before, but now the ``index`` view looks like this:: @app.route("/") def index(): if not github.authorized: return redirect(url_for("github.login")) resp = github.get("/user") return "You are @{login} on GitHub".format(login=resp.json()["login"]) Here's how you could test this view using Betamax:: import os from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.github import github import pytest from betamax import Betamax from myapp import app as _app from myapp import github_bp with Betamax.configure() as config: config.cassette_library_dir = 'cassettes' @pytest.fixture def app(): return _app @pytest.fixture def betamax_github(app, request): @app.before_request def wrap_github_with_betamax(): recorder = Betamax(github) recorder.use_cassette(request.node.name) recorder.start() @app.after_request def unwrap(response): recorder.stop() return response request.addfinalizer( lambda: app.after_request_funcs[None].remove(unwrap) ) request.addfinalizer( lambda: app.before_request_funcs[None].remove(wrap_github_with_betamax) ) return app @pytest.mark.usefixtures("betamax_github") def test_index_authorized(app, monkeypatch): access_token = os.environ.get("GITHUB_OAUTH_ACCESS_TOKEN", "fake-token") storage = MemoryStorage({"access_token": access_token}) monkeypatch.setattr(github_bp, "storage", storage) with app.test_client() as client: response = client.get("/", base_url="https://example.com") assert response.status_code == 200 text = response.get_data(as_text=True) assert text == "You are @singingwolfboy on GitHub" In this example, we first :doc:`configure Betamax globally ` so that it stores cassettes (recorded HTTP interactions) in the ``cassettes`` directory. Betamax expects you to commit these cassettes to your repository, so that if the HTTP interactions change, that will show up in code review. Next, we define a utility function that will wrap Betamax around the ``github`` :class:`~requests.Session` object at the start of the incoming HTTP request, and unwrap it afterwards. This allows Betamax to record and intercept HTTP requests during the test. Note that we also use ``request.addfinalizer`` to remove these "before_request" and "after_request" functions, so that they don't interfere with other tests. If you are recreating your ``app`` object from scratch each time using :doc:`the application factory pattern `, you don't need to include these ``request.addfinalizer`` lines. In the actual test, we check for the :envvar:`GITHUB_OAUTH_ACCESS_TOKEN` environment variable. When recording a cassette with Betamax, it will send real HTTP requests to the OAuth provider, so you'll need to include a real OAuth access token if you expect the API call to succeed. However, once the cassette has been recorded, you can re-run the tests without setting this environment variable. Also notice that you can (and should!) make assertions in your test that expect a particular API response. In this test, I assert that the current user is named ``@singingwolfboy``. I can do that, because when I recorded the cassette, that was the GitHub user that I used. When the cassette is replayed in the future, the API response will always be the same, so I can write my assertions expecting that. Provided Pytest Fixture ----------------------- .. automodule:: flask_dance.fixtures.pytest .. _pytest: https://docs.pytest.org/ .. _Betamax: https://github.com/betamaxpy/betamax .. _Requests: https://requests.kennethreitz.org/ .. _@sigmavirus24: https://github.com/sigmavirus24/ flask-dance-7.1.0/docs/understanding-the-magic.rst000066400000000000000000000113551457161140100220570ustar00rootroot00000000000000Understanding the Magic ======================= .. currentmodule:: flask_dance.consumer Flask-Dance might initially seem like magic ("it just works!"), but it's just code. It's complicated, but understandable. This page will teach you how Flask-Dance works. Making the Blueprint -------------------- The first thing you do with Flask-Dance is make a blueprint. This is an instance of :class:`OAuth1ConsumerBlueprint` or :class:`OAuth2ConsumerBlueprint`, depending on if you're using OAuth 1 or OAuth 2. (Most providers use OAuth 2.) When you make your blueprint, you can either pass your client ID and client secret to the blueprint directly, or teach your blueprint where to find those values on its own using the :attr:`~OAuth2ConsumerBlueprint.from_config` dictionary. Using this dictionary is usually a good idea, since it allows you to specify these values in your application configuration instead of in your code. After you've made the blueprint, you need to register it on your Flask application, just like you would with any other blueprint. Using the Requests Session -------------------------- The Flask-Dance blueprints have a :attr:`~OAuth2ConsumerBlueprint.session` attribute. When you access this attribute, the blueprint will create and return a :class:`requests.Session` object, properly configured for OAuth authentication. You can use this object in exactly the same way as you would normally use the Requests library for making HTTP requests. The pre-set configurations also allow you to import special objects that refer to these Requests session objects. For example, if you run this code: .. code-block:: python from flask_dance.contrib.github import github You can then call ``github.get()`` just like you do with Requests. However, this ``github`` object is not actually a Requests session -- it's something called a :class:`~werkzeug.local.LocalProxy`. This allows you to access the session within the context of an incoming HTTP request, but it will *not* allow you access it outside that context. Checking Authorization ---------------------- When your application starts up, Flask-Dance will check your token storage to see if there is an OAuth token already saved there. If so, the ``authorized`` property on your Requests Session object will be ``True``; if not, it will be ``False``. You can use this to determine if the user needs to go through the OAuth dance or not. .. warning:: If the OAuth token is expired or invalid, it will not work. However, this ``authorized`` property can not check this for you! It only checks if the token *exists*. Starting the Dance ------------------ In order to start the OAuth dance, redirect the user to the :meth:`~OAuth2ConsumerBlueprint.login` view from your blueprint. You will need to provide the name of your blueprint when calling Flask's :func:`~flask.url_for` function. For example, for the GitHub contrib: .. code-block:: python from flask import redirect, url_for def my_view_func(): # ... implement whatever logic you want here return redirect(url_for("github.login")) State & Security ~~~~~~~~~~~~~~~~ One of the key features of :attr:`OAuth2ConsumerBlueprint.session` is that the requests it generates use a ``state`` variable to ensure that the source of OAuth authorization callbacks is in fact your intended OAuth provider. By default, the state is a random 30-character string, as provided by :func:`oauthlib.common.generate_token`. This protects your app against one kind of CSRF attack. For more information, see `section 10.12 of the OAuth 2 spec `_. Finishing the Dance ------------------- After the user finishes the OAuth dance, they will be redirected back to the :meth:`~OAuth2ConsumerBlueprint.authorized` view from your blueprint. This will save the OAuth token to whatever token storage you are using, and will then redirect the user to a different page on your website. By default, the user will be redirected back to the root page (``/``). However, you can set the ``redirect_url`` or ``redirect_to`` arguments in your blueprint to change this. If you want a dynamic redirect, where the URL isn't known until the user finishes the OAuth dance, hook into the :data:`~flask_dance.consumer.oauth_authorized` signal and return the redirect from your subscriber function. For example: .. code-block:: python import flask from flask_dance.consumer import oauth_authorized @oauth_authorized.connect def redirect_to_next_url(blueprint, token): # set OAuth token in the token storage backend blueprint.token = token # retrieve `next_url` from Flask's session cookie next_url = flask.session["next_url"] # redirect the user to `next_url` return flask.redirect(next_url) flask-dance-7.1.0/flask_dance/000077500000000000000000000000001457161140100161215ustar00rootroot00000000000000flask-dance-7.1.0/flask_dance/__init__.py000066400000000000000000000002451457161140100202330ustar00rootroot00000000000000"Doing the OAuth dance with style using Flask, requests, and oauthlib" from .consumer import OAuth1ConsumerBlueprint, OAuth2ConsumerBlueprint __version__ = "7.1.0" flask-dance-7.1.0/flask_dance/consumer/000077500000000000000000000000001457161140100177545ustar00rootroot00000000000000flask-dance-7.1.0/flask_dance/consumer/__init__.py000066400000000000000000000003171457161140100220660ustar00rootroot00000000000000from .base import oauth_authorized, oauth_before_login, oauth_error from .oauth1 import OAuth1ConsumerBlueprint from .oauth2 import OAuth2ConsumerBlueprint from .requests import OAuth1Session, OAuth2Session flask-dance-7.1.0/flask_dance/consumer/base.py000066400000000000000000000136451457161140100212510ustar00rootroot00000000000000from abc import ABCMeta, abstractmethod, abstractproperty from datetime import datetime, timedelta, timezone import flask from flask.signals import Namespace from werkzeug.datastructures import CallbackDict from flask_dance.consumer.storage.session import SessionStorage from flask_dance.utils import getattrd _signals = Namespace() oauth_authorized = _signals.signal("oauth-authorized") oauth_before_login = _signals.signal("oauth-before-login") oauth_error = _signals.signal("oauth-error") class BaseOAuthConsumerBlueprint(flask.Blueprint, metaclass=ABCMeta): def __init__( self, name, import_name, *, static_folder=None, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, root_path=None, login_url=None, authorized_url=None, storage=None, rule_kwargs=None, ): bp_kwargs = dict( name=name, import_name=import_name, static_folder=static_folder, static_url_path=static_url_path, template_folder=template_folder, url_prefix=url_prefix, subdomain=subdomain, url_defaults=url_defaults, root_path=root_path, ) flask.Blueprint.__init__(self, **bp_kwargs) login_url = login_url or "/{bp.name}" authorized_url = authorized_url or "/{bp.name}/authorized" rule_kwargs = rule_kwargs or {} self.add_url_rule( rule=login_url.format(bp=self), endpoint="login", view_func=self.login, **rule_kwargs, ) self.add_url_rule( rule=authorized_url.format(bp=self), endpoint="authorized", view_func=self.authorized, **rule_kwargs, ) if storage is None: self.storage = SessionStorage() elif callable(storage): self.storage = storage() else: self.storage = storage self.logged_in_funcs = [] self.from_config = {} def invalidate_token(d): try: del self.session.token except KeyError: pass self.config = CallbackDict(on_update=invalidate_token) self.before_app_request(self.load_config) def load_config(self): """ Used to dynamically load variables from the Flask application config into the blueprint. To tell this blueprint to pull configuration from the app, just set key-value pairs in the ``from_config`` dict. Keys are the name of the local variable to set on the blueprint object, and values are the variable name in the Flask application config. For example: blueprint.from_config["session.client_id"] = "GITHUB_OAUTH_CLIENT_ID" """ for local_var, config_var in self.from_config.items(): value = flask.current_app.config.get(config_var) if value: if "." in local_var: # this is a dotpath -- needs special handling body, tail = local_var.rsplit(".", 1) obj = getattrd(self, body) setattr(obj, tail, value) else: # just use a normal setattr call setattr(self, local_var, value) @property def storage(self): """ The :doc:`token storage ` that this blueprint uses. """ return self._storage @storage.setter def storage(self, value): self._storage = value @storage.deleter def storage(self): del self._storage @property def token(self): """ This property functions as pass-through to the token storage. If you read from this property, you will receive the current value from the token storage. If you assign a value to this property, it will get set in the token storage. """ _token = self.storage.get(self) if _token and _token.get("expires_in") and _token.get("expires_at"): # Update the `expires_in` value, so that requests-oauthlib # can handle automatic token refreshing. Assume that # `expires_at` is a valid Unix timestamp. expires_at = datetime.fromtimestamp(_token["expires_at"], timezone.utc) expires_in = expires_at - datetime.now(timezone.utc) _token["expires_in"] = expires_in.total_seconds() return _token @token.setter def token(self, value): _token = value if _token and _token.get("expires_in"): # Set the `expires_at` value, overwriting any value # that may already be there. delta = timedelta(seconds=int(_token["expires_in"])) expires_at = datetime.now(timezone.utc) + delta _token["expires_at"] = expires_at.replace(tzinfo=timezone.utc).timestamp() self.storage.set(self, _token) try: del self.session.token except KeyError: pass @token.deleter def token(self): self.storage.delete(self) try: del self.session.token except KeyError: pass @abstractproperty def session(self): """ This is a session between the consumer (your website) and the provider (e.g. Google). It is *not* a session between a user of your website and your website. """ raise NotImplementedError() @abstractmethod def login(self): raise NotImplementedError() @abstractmethod def authorized(self): """ This is the route/function that the user will be redirected to by the provider (e.g. Google) after the user has logged into the provider's website and authorized your app to access their account. """ raise NotImplementedError() flask-dance-7.1.0/flask_dance/consumer/oauth1.py000066400000000000000000000231261457161140100215330ustar00rootroot00000000000000import logging from flask import current_app, redirect, request, url_for from oauthlib.common import to_unicode from oauthlib.oauth1 import SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER from requests_oauthlib.oauth1_session import TokenMissing, TokenRequestDenied from werkzeug.utils import cached_property from werkzeug.wrappers import Response from .base import ( BaseOAuthConsumerBlueprint, oauth_authorized, oauth_before_login, oauth_error, ) from .requests import OAuth1Session log = logging.getLogger(__name__) class OAuth1ConsumerBlueprint(BaseOAuthConsumerBlueprint): """ A subclass of :class:`flask.Blueprint` that sets up OAuth 1 authentication. """ def __init__( self, name, import_name, client_key=None, client_secret=None, *, signature_method=SIGNATURE_HMAC, signature_type=SIGNATURE_TYPE_AUTH_HEADER, rsa_key=None, client_class=None, force_include_body=False, static_folder=None, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, root_path=None, login_url=None, authorized_url=None, base_url=None, request_token_url=None, authorization_url=None, access_token_url=None, redirect_url=None, redirect_to=None, session_class=None, storage=None, rule_kwargs=None, **kwargs, ): """ Most of the constructor arguments are forwarded either to the :class:`flask.Blueprint` constructor or the :class:`requests_oauthlib.OAuth1Session` constructor, including ``**kwargs`` (which is forwarded to :class:`~requests_oauthlib.OAuth1Session`). Only the arguments that are relevant to Flask-Dance are documented here. Args: base_url: The base URL of the OAuth provider. If specified, all URLs passed to this instance will be resolved relative to this URL. request_token_url: The URL specified by the OAuth provider for obtaining a `request token `_. This can be an fully-qualified URL, or a path that is resolved relative to the ``base_url``. authorization_url: The URL specified by the OAuth provider for the user to `grant token authorization `_. This can be an fully-qualified URL, or a path that is resolved relative to the ``base_url``. access_token_url: The URL specified by the OAuth provider for obtaining an `access token `_. This can be an fully-qualified URL, or a path that is resolved relative to the ``base_url``. login_url: The URL route for the ``login`` view that kicks off the OAuth dance. This string will be :ref:`formatted ` with the instance so that attributes can be interpolated. Defaults to ``/{bp.name}``, so that the URL is based on the name of the blueprint. authorized_url: The URL route for the ``authorized`` view that completes the OAuth dance. This string will be :ref:`formatted ` with the instance so that attributes can be interpolated. Defaults to ``/{bp.name}/authorized``, so that the URL is based on the name of the blueprint. redirect_url: When the OAuth dance is complete, redirect the user to this URL. redirect_to: When the OAuth dance is complete, redirect the user to the URL obtained by calling :func:`~flask.url_for` with this argument. If you do not specify either ``redirect_url`` or ``redirect_to``, the user will be redirected to the root path (``/``). session_class: The class to use for creating a Requests session between the consumer (your website) and the provider (e.g. Google). Defaults to :class:`~flask_dance.consumer.requests.OAuth1Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. """ BaseOAuthConsumerBlueprint.__init__( self, name, import_name, static_folder=static_folder, static_url_path=static_url_path, template_folder=template_folder, url_prefix=url_prefix, subdomain=subdomain, url_defaults=url_defaults, root_path=root_path, login_url=login_url, authorized_url=authorized_url, storage=storage, rule_kwargs=rule_kwargs, ) self.base_url = base_url self.session_class = session_class or OAuth1Session # passed to OAuth1Session() self.client_key = client_key self.client_secret = client_secret self.signature_method = signature_method self.signature_type = signature_type self.rsa_key = rsa_key self.client_class = client_class self.force_include_body = force_include_body self.kwargs = kwargs # used by view functions self.request_token_url = request_token_url self.authorization_url = authorization_url self.access_token_url = access_token_url self.redirect_url = redirect_url self.redirect_to = redirect_to self.teardown_app_request(self.teardown_session) @cached_property def session(self): """ This is a session between the consumer (your website) and the provider (e.g. Google). It is *not* a session between a user of your website and your website. :return: """ return self.session_class( client_key=self.client_key, client_secret=self.client_secret, signature_method=self.signature_method, signature_type=self.signature_type, rsa_key=self.rsa_key, client_class=self.client_class, force_include_body=self.force_include_body, blueprint=self, base_url=self.base_url, **self.kwargs, ) def teardown_session(self, exception=None): try: del self.session except KeyError: pass def login(self): callback_uri = url_for(".authorized", _external=True) self.session._client.client.callback_uri = to_unicode(callback_uri) try: self.session.fetch_request_token( self.request_token_url, should_load_token=False ) except TokenRequestDenied as err: message = err.args[0] response = getattr(err, "response", None) log.warning("OAuth 1 request token error: %s", message) oauth_error.send(self, message=message, response=response) # can't proceed with OAuth, have to just redirect to next_url if self.redirect_url: next_url = self.redirect_url elif self.redirect_to: next_url = url_for(self.redirect_to) else: next_url = "/" return redirect(next_url) url = self.session.authorization_url(self.authorization_url) oauth_before_login.send(self, url=url) return redirect(url) def authorized(self): """ This is the route/function that the user will be redirected to by the provider (e.g. Google) after the user has logged into the provider's website and authorized your app to access their account. """ if self.redirect_url: next_url = self.redirect_url elif self.redirect_to: next_url = url_for(self.redirect_to) else: next_url = "/" try: self.session.parse_authorization_response(request.url) except TokenMissing as err: message = err.args[0] response = getattr(err, "response", None) log.warning("OAuth 1 access token error: %s", message) oauth_error.send(self, message=message, response=response) return redirect(next_url) try: token = self.session.fetch_access_token( self.access_token_url, should_load_token=False ) except ValueError as err: # can't proceed with OAuth, have to just redirect to next_url message = err.args[0] response = getattr(err, "response", None) log.warning("OAuth 1 access token error: %s", message) oauth_error.send(self, message=message, response=response) return redirect(next_url) results = oauth_authorized.send(self, token=token) or [] set_token = True for func, ret in results: if isinstance(ret, (Response, current_app.response_class)): return ret if ret == False: set_token = False if set_token: self.token = token return redirect(next_url) flask-dance-7.1.0/flask_dance/consumer/oauth2.py000066400000000000000000000314031457161140100215310ustar00rootroot00000000000000import json import logging import flask from flask import current_app, redirect, request, url_for from oauthlib.common import generate_token from oauthlib.oauth2 import MissingCodeError from werkzeug.utils import cached_property from werkzeug.wrappers import Response from .base import ( BaseOAuthConsumerBlueprint, oauth_authorized, oauth_before_login, oauth_error, ) from .requests import OAuth2Session log = logging.getLogger(__name__) class OAuth2ConsumerBlueprint(BaseOAuthConsumerBlueprint): """ A subclass of :class:`flask.Blueprint` that sets up OAuth 2 authentication. """ def __init__( self, name, import_name, client_id=None, client_secret=None, *, client=None, auto_refresh_url=None, auto_refresh_kwargs=None, scope=None, state=None, static_folder=None, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, root_path=None, login_url=None, authorized_url=None, base_url=None, authorization_url=None, authorization_url_params=None, token_url=None, token_url_params=None, redirect_url=None, redirect_to=None, session_class=None, storage=None, rule_kwargs=None, use_pkce=False, code_challenge_method="S256", **kwargs, ): """ Most of the constructor arguments are forwarded either to the :class:`flask.Blueprint` constructor or the :class:`requests_oauthlib.OAuth2Session` constructor, including ``**kwargs`` (which is forwarded to :class:`~requests_oauthlib.OAuth2Session`). Only the arguments that are relevant to Flask-Dance are documented here. Args: base_url: The base URL of the OAuth provider. If specified, all URLs passed to this instance will be resolved relative to this URL. authorization_url: The URL specified by the OAuth provider for obtaining an `authorization grant `__. This can be an fully-qualified URL, or a path that is resolved relative to the ``base_url``. authorization_url_params (dict): A dict of extra key-value pairs to include in the query string of the ``authorization_url``, beyond those necessary for a standard OAuth 2 authorization grant request. token_url: The URL specified by the OAuth provider for obtaining an `access token `__. This can be an fully-qualified URL, or a path that is resolved relative to the ``base_url``. token_url_params (dict): A dict of extra key-value pairs to include in the query string of the ``token_url``, beyond those necessary for a standard OAuth 2 access token request. login_url: The URL route for the ``login`` view that kicks off the OAuth dance. This string will be :ref:`formatted ` with the instance so that attributes can be interpolated. Defaults to ``/{bp.name}``, so that the URL is based on the name of the blueprint. authorized_url: The URL route for the ``authorized`` view that completes the OAuth dance. This string will be :ref:`formatted ` with the instance so that attributes can be interpolated. Defaults to ``/{bp.name}/authorized``, so that the URL is based on the name of the blueprint. redirect_url: When the OAuth dance is complete, redirect the user to this URL. redirect_to: When the OAuth dance is complete, redirect the user to the URL obtained by calling :func:`~flask.url_for` with this argument. If you do not specify either ``redirect_url`` or ``redirect_to``, the user will be redirected to the root path (``/``). session_class: The class to use for creating a Requests session between the consumer (your website) and the provider (e.g. Google). Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. use_pkce: If true then the authorization flow will follow the PKCE (Proof Key for Code Exchange). For more details please refer to `RFC7636 `__ code_challenge_method: Code challenge method to be used in authorization code flow with PKCE instead of client secret. It will be used only if ``use_pkce`` is set to True. Defaults to ``S256``. """ BaseOAuthConsumerBlueprint.__init__( self, name, import_name, static_folder=static_folder, static_url_path=static_url_path, template_folder=template_folder, url_prefix=url_prefix, subdomain=subdomain, url_defaults=url_defaults, root_path=root_path, login_url=login_url, authorized_url=authorized_url, storage=storage, rule_kwargs=rule_kwargs, ) self.base_url = base_url self.session_class = session_class or OAuth2Session # passed to OAuth2Session() self._client_id = client_id self.client = client self.auto_refresh_url = auto_refresh_url self.auto_refresh_kwargs = auto_refresh_kwargs self.scope = scope self.state = state self.kwargs = kwargs self.client_secret = client_secret # used by view functions self.authorization_url = authorization_url self.authorization_url_params = authorization_url_params or {} self.token_url = token_url self.token_url_params = token_url_params or {} self.redirect_url = redirect_url self.redirect_to = redirect_to self.code_challenge_method = code_challenge_method self.use_pkce = use_pkce self.teardown_app_request(self.teardown_session) @property def client_id(self): return self.session.client_id @client_id.setter def client_id(self, value): self.session.client_id = value # due to a bug in requests-oauthlib, we need to set this manually self.session._client.client_id = value @cached_property def session(self): """ This is a session between the consumer (your website) and the provider (e.g. Google). It is *not* a session between a user of your website and your website. :return: """ ret = self.session_class( client_id=self._client_id, client=self.client, auto_refresh_url=self.auto_refresh_url, auto_refresh_kwargs=self.auto_refresh_kwargs, scope=self.scope, state=self.state, blueprint=self, base_url=self.base_url, **self.kwargs, ) def token_updater(token): self.token = token ret.token_updater = token_updater return self.session_created(ret) def session_created(self, session): return session def teardown_session(self, exception=None): try: del self.session except KeyError: pass def login(self): log.debug("client_id = %s", self.client_id) self.session.redirect_uri = url_for(".authorized", _external=True) if self.use_pkce: code_verifier = generate_token(length=48) code_challenge = self.session._client.create_code_challenge( code_verifier=code_verifier, code_challenge_method=self.code_challenge_method, ) self.authorization_url_params.update( { "code_challenge_method": self.code_challenge_method, "code_challenge": code_challenge, } ) code_verifier_key = f"{self.name}_oauth_code_verifier" flask.session[code_verifier_key] = code_verifier log.debug("code_verifier = %s", code_verifier) url, state = self.session.authorization_url( self.authorization_url, state=self.state, **self.authorization_url_params ) state_key = f"{self.name}_oauth_state" flask.session[state_key] = state log.debug("state = %s", state) log.debug("redirect URL = %s", url) oauth_before_login.send(self, url=url) return redirect(url) def authorized(self): """ This is the route/function that the user will be redirected to by the provider (e.g. Google) after the user has logged into the provider's website and authorized your app to access their account. """ if self.redirect_url: next_url = self.redirect_url elif self.redirect_to: next_url = url_for(self.redirect_to) else: next_url = "/" log.debug("next_url = %s", next_url) # check for error in request args error = request.args.get("error") if error: error_desc = request.args.get("error_description") error_uri = request.args.get("error_uri") log.warning( "OAuth 2 authorization error: %s description: %s uri: %s", error, error_desc, error_uri, ) results = oauth_error.send( self, error=error, error_description=error_desc, error_uri=error_uri ) if results: for _, ret in results: if isinstance(ret, (Response, current_app.response_class)): return ret return redirect(next_url) state_key = f"{self.name}_oauth_state" if state_key not in flask.session: # can't validate state, so redirect back to login view log.info("state not found, redirecting user to login") return redirect(url_for(".login")) state = flask.session[state_key] log.debug("state = %s", state) self.session._state = state del flask.session[state_key] if self.use_pkce: code_verifier_key = f"{self.name}_oauth_code_verifier" if code_verifier_key not in flask.session: # can't find code_verifier, so redirect back to login view log.info("code_verifier not found, redirecting user to login") return redirect(url_for(".login")) code_verifier = flask.session[code_verifier_key] log.debug("code_verifier = %s", code_verifier) del flask.session[code_verifier_key] self.token_url_params["code_verifier"] = code_verifier self.session.redirect_uri = url_for(".authorized", _external=True) log.debug("client_id = %s", self.client_id) log.debug("client_secret = %s", self.client_secret) try: token = self.session.fetch_token( self.token_url, authorization_response=request.url, client_secret=self.client_secret, **self.token_url_params, ) except MissingCodeError as e: e.args = ( e.args[0], "The redirect request did not contain the expected parameters. Instead I got: {}".format( json.dumps(request.args) ), ) raise results = oauth_authorized.send(self, token=token) or [] set_token = True for func, ret in results: if isinstance(ret, (Response, current_app.response_class)): return ret if ret == False: set_token = False if set_token: try: self.token = token except ValueError as error: log.warning("OAuth 2 authorization error: %s", str(error)) oauth_error.send(self, error=error) return redirect(next_url) flask-dance-7.1.0/flask_dance/consumer/requests.py000066400000000000000000000157621457161140100222140ustar00rootroot00000000000000from functools import wraps from flask import redirect, url_for from oauthlib.common import to_unicode from requests_oauthlib import OAuth1Session as BaseOAuth1Session from requests_oauthlib import OAuth2Session as BaseOAuth2Session from urlobject import URLObject from werkzeug.utils import cached_property class OAuth1Session(BaseOAuth1Session): """ A :class:`requests.Session` subclass that can do some special things: * lazy-loads OAuth1 tokens from the storage via the blueprint * handles OAuth1 authentication (from :class:`requests_oauthlib.OAuth1Session` superclass) * has a ``base_url`` property used for relative URL resolution Note that this is a session between the consumer (your website) and the provider (e.g. Google), and *not* a session between a user of your website and your website. """ def __init__(self, blueprint=None, base_url=None, *args, **kwargs): super().__init__(*args, **kwargs) self.blueprint = blueprint self.base_url = URLObject(base_url) @cached_property def token(self): """ Get and set the values in the OAuth token, structured as a dictionary. """ return self.blueprint.token def load_token(self): t = self.token if t and "oauth_token" in t and "oauth_token_secret" in t: # This really, really violates the Law of Demeter, but # I don't see a better way to set these parameters. :( self.auth.client.resource_owner_key = to_unicode(t["oauth_token"]) self.auth.client.resource_owner_secret = to_unicode(t["oauth_token_secret"]) return True return False @property def authorized(self): """This is the property used when you have a statement in your code that reads "if .authorized:", e.g. "if google.authorized:". The way it works is kind of complicated: this function just tries to load the token, and then the 'super()' statement basically just tests if the token exists (see BaseOAuth1Session.authorized). To load the token, it calls the load_token() function within this class, which in turn checks the 'token' property of this class (another function), which in turn checks the 'token' property of the blueprint (see base.py), which calls 'storage.get()' to actually try to load the token from the cache/db (see the 'get()' function in storage/sqla.py). """ self.load_token() return super().authorized @property def authorization_required(self): """ .. versionadded:: 1.3.0 This is a decorator for a view function. If the current user does not have an OAuth token, then they will be redirected to the :meth:`~flask_dance.consumer.oauth1.OAuth1ConsumerBlueprint.login` view to obtain one. """ def wrapper(func): @wraps(func) def check_authorization(*args, **kwargs): if not self.authorized: endpoint = f"{self.blueprint.name}.login" return redirect(url_for(endpoint)) return func(*args, **kwargs) return check_authorization return wrapper def prepare_request(self, request): if self.base_url: request.url = self.base_url.relative(request.url) return super().prepare_request(request) def request( self, method, url, data=None, headers=None, should_load_token=True, **kwargs ): if should_load_token: self.load_token() return super().request( method=method, url=url, data=data, headers=headers, **kwargs ) class OAuth2Session(BaseOAuth2Session): """ A :class:`requests.Session` subclass that can do some special things: * lazy-loads OAuth2 tokens from the storage via the blueprint * handles OAuth2 authentication (from :class:`requests_oauthlib.OAuth2Session` superclass) * has a ``base_url`` property used for relative URL resolution Note that this is a session between the consumer (your website) and the provider (e.g. Google), and *not* a session between a user of your website and your website. """ def __init__(self, blueprint=None, base_url=None, *args, **kwargs): super().__init__(*args, **kwargs) self.blueprint = blueprint self.base_url = URLObject(base_url) del self.token @cached_property def token(self): """ Get and set the values in the OAuth token, structured as a dictionary. """ return self.blueprint.token def load_token(self): self._client.token = self.token if self.token: self._client.populate_token_attributes(self.token) return True return False @property def access_token(self): """ Returns the ``access_token`` from the OAuth token. """ return self.token and self.token.get("access_token") @property def authorized(self): """This is the property used when you have a statement in your code that reads "if .authorized:", e.g. "if google.authorized:". The way it works is kind of complicated: this function just tries to load the token, and then the 'super()' statement basically just tests if the token exists (see BaseOAuth1Session.authorized). To load the token, it calls the load_token() function within this class, which in turn checks the 'token' property of this class (another function), which in turn checks the 'token' property of the blueprint (see base.py), which calls 'storage.get()' to actually try to load the token from the cache/db (see the 'get()' function in storage/sqla.py). """ self.load_token() return super().authorized @property def authorization_required(self): """ .. versionadded:: 1.3.0 This is a decorator for a view function. If the current user does not have an OAuth token, then they will be redirected to the :meth:`~flask_dance.consumer.oauth2.OAuth2ConsumerBlueprint.login` view to obtain one. """ def wrapper(func): @wraps(func) def check_authorization(*args, **kwargs): if not self.authorized: endpoint = f"{self.blueprint.name}.login" return redirect(url_for(endpoint)) return func(*args, **kwargs) return check_authorization return wrapper def request(self, method, url, data=None, headers=None, **kwargs): if self.base_url: url = self.base_url.relative(url) self.load_token() return super().request( method=method, url=url, data=data, headers=headers, client_id=self.blueprint.client_id, client_secret=self.blueprint.client_secret, **kwargs, ) flask-dance-7.1.0/flask_dance/consumer/storage/000077500000000000000000000000001457161140100214205ustar00rootroot00000000000000flask-dance-7.1.0/flask_dance/consumer/storage/__init__.py000066400000000000000000000023631457161140100235350ustar00rootroot00000000000000from abc import ABCMeta, abstractmethod class BaseStorage(metaclass=ABCMeta): @abstractmethod def get(self, blueprint): return None @abstractmethod def set(self, blueprint, token): return None @abstractmethod def delete(self, blueprint): return None class NullStorage(BaseStorage): """ This mock storage will never store OAuth tokens. If you try to retrieve a token from this storage, you will always get ``None``. """ def get(self, blueprint): return None def set(self, blueprint, token): return None def delete(self, blueprint): return None class MemoryStorage(BaseStorage): """ This mock storage stores an OAuth token in memory and so that it can be retrieved later. Since the token is not persisted in any way, this is mostly useful for writing automated tests. The initializer accepts a ``token`` argument, for setting the initial value of the token. """ def __init__(self, token=None, *args, **kwargs): self.token = token def get(self, blueprint): return self.token def set(self, blueprint, token): self.token = token def delete(self, blueprint): self.token = None flask-dance-7.1.0/flask_dance/consumer/storage/session.py000066400000000000000000000020761457161140100234620ustar00rootroot00000000000000import flask from flask_dance.consumer.storage import BaseStorage class SessionStorage(BaseStorage): """ The default storage backend. Stores and retrieves OAuth tokens using the :ref:`Flask session `. """ def __init__(self, key="{bp.name}_oauth_token"): """ Args: key (str): The name to use as a key for storing the OAuth token in the Flask session. This string will have ``.format(bp=self.blueprint)`` called on it before it is used. so you can refer to information on the blueprint as part of the key. For example, ``{bp.name}`` will be replaced with the name of the blueprint. """ self.key = key def get(self, blueprint): key = self.key.format(bp=blueprint) return flask.session.get(key) def set(self, blueprint, token): key = self.key.format(bp=blueprint) flask.session[key] = token def delete(self, blueprint): key = self.key.format(bp=blueprint) del flask.session[key] flask-dance-7.1.0/flask_dance/consumer/storage/sqla.py000066400000000000000000000255731457161140100227460ustar00rootroot00000000000000from datetime import datetime from sqlalchemy import JSON, Column, DateTime, Integer, String from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.mutable import MutableDict from sqlalchemy.orm.exc import NoResultFound from flask_dance.consumer.storage import BaseStorage from flask_dance.utils import FakeCache, first try: from flask_login import AnonymousUserMixin except ImportError: AnonymousUserMixin = None class OAuthConsumerMixin: """ A :ref:`SQLAlchemy declarative mixin ` with some suggested columns for a model to store OAuth tokens: ``id`` an integer primary key ``provider`` a short name to indicate which OAuth provider issued this token ``created_at`` an automatically generated datetime that indicates when the OAuth provider issued this token ``token`` a :class:`JSON ` field to store the actual token received from the OAuth provider """ @declared_attr def __tablename__(cls): return f"flask_dance_{cls.__name__.lower()}" id = Column(Integer, primary_key=True) provider = Column(String(50), nullable=False) created_at = Column(DateTime, default=datetime.utcnow, nullable=False) token = Column(MutableDict.as_mutable(JSON), nullable=False) def __repr__(self): parts = [] parts.append(self.__class__.__name__) if self.id: parts.append(f"id={self.id}") if self.provider: parts.append(f'provider="{self.provider}"') return "<{}>".format(" ".join(parts)) class SQLAlchemyStorage(BaseStorage): """ Stores and retrieves OAuth tokens using a relational database through the `SQLAlchemy`_ ORM. .. _SQLAlchemy: http://www.sqlalchemy.org/ """ def __init__( self, model, session, user=None, user_id=None, user_required=None, anon_user=None, cache=None, ): """ Args: model: The SQLAlchemy model class that represents the OAuth token table in the database. At a minimum, it must have a ``provider`` column and a ``token`` column. If tokens are to be associated with individual users in the application, it must also have a ``user`` relationship to your User model. It is recommended, though not required, that your model class inherit from :class:`~flask_dance.consumer.storage.sqla.OAuthConsumerMixin`. session: The :class:`SQLAlchemy session ` for the database. If you're using `Flask-SQLAlchemy`_, this is ``db.session``. user: If you want OAuth tokens to be associated with individual users in your application, this is a reference to the user that you want to use for the current request. It can be an actual User object, a function that returns a User object, or a proxy to the User object. If you're using `Flask-Login`_, this is :attr:`~flask.ext.login.current_user`. user_id: If you want to pass an identifier for a user instead of an actual User object, use this argument instead. Sometimes it can save a database query or two. If both ``user`` and ``user_id`` are provided, ``user_id`` will take precendence. user_required: If set to ``True``, an exception will be raised if you try to set or retrieve an OAuth token without an associated user. If set to ``False``, OAuth tokens can be set with or without an associated user. The default is auto-detection: it will be ``True`` if you pass a ``user`` or ``user_id`` parameter, ``False`` otherwise. anon_user: If anonymous users are represented by a class in your application, provide that class here. If you are using `Flask-Login`_, anonymous users are represented by the :class:`flask_login.AnonymousUserMixin` class, but you don't have to provide that -- Flask-Dance treats it as the default. cache: An instance of `Flask-Caching`_. Providing a caching system is highly recommended, but not required. .. _Flask-SQLAlchemy: http://pythonhosted.org/Flask-SQLAlchemy/ .. _Flask-Login: https://flask-login.readthedocs.io/ .. _Flask-Caching: https://flask-caching.readthedocs.io/en/latest/ """ self.model = model self.session = session self.user = user self.user_id = user_id if user_required is None: self.user_required = user is not None or user_id is not None else: self.user_required = user_required self.anon_user = anon_user or AnonymousUserMixin self.cache = cache or FakeCache() def make_cache_key(self, blueprint, user=None, user_id=None): uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) if not uid: u = first( _get_real_user(ref, self.anon_user) for ref in (user, self.user, blueprint.config.get("user")) ) uid = getattr(u, "id", u) return "flask_dance_token|{name}|{user_id}".format( name=blueprint.name, user_id=uid ) def get(self, blueprint, user=None, user_id=None): """When you have a statement in your code that says "if .authorized:" (for example "if google.authorized:"), a long string of function calls result in this function being used to check the Flask server's cache and database for any records associated with the current_user. The `user` and `user_id` parameters are actually not set in that case (see base.py:token(), that's what calls this function), so the user information is instead loaded from the current_user (if that's what you specified when you created the blueprint) with blueprint.config.get('user_id'). :param blueprint: :param user: :param user_id: :return: """ # check cache cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id) token = self.cache.get(cache_key) if token: return token # if not cached, make database queries query = self.session.query(self.model).filter_by(provider=blueprint.name) uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) u = first( _get_real_user(ref, self.anon_user) for ref in (user, self.user, blueprint.config.get("user")) ) if self.user_required and not u and not uid: raise ValueError("Cannot get OAuth token without an associated user") # check for user ID if hasattr(self.model, "user_id") and uid: query = query.filter_by(user_id=uid) # check for user (relationship property) elif hasattr(self.model, "user") and u: query = query.filter_by(user=u) # if we have the property, but not value, filter by None elif hasattr(self.model, "user_id"): query = query.filter_by(user_id=None) # run query try: token = query.one().token except NoResultFound: token = None # cache the result self.cache.set(cache_key, token) return token def set(self, blueprint, token, user=None, user_id=None): uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) u = first( _get_real_user(ref, self.anon_user) for ref in (user, self.user, blueprint.config.get("user")) ) if self.user_required and not u and not uid: raise ValueError("Cannot set OAuth token without an associated user") # if there was an existing model, delete it existing_query = self.session.query(self.model).filter_by( provider=blueprint.name ) # check for user ID has_user_id = hasattr(self.model, "user_id") if has_user_id and uid: existing_query = existing_query.filter_by(user_id=uid) # check for user (relationship property) has_user = hasattr(self.model, "user") if has_user and u: existing_query = existing_query.filter_by(user=u) # queue up delete query -- won't be run until commit() existing_query.delete() # create a new model for this token kwargs = {"provider": blueprint.name, "token": token} if has_user_id and uid: kwargs["user_id"] = uid if has_user and u: kwargs["user"] = u self.session.add(self.model(**kwargs)) # commit to delete and add simultaneously self.session.commit() # invalidate cache self.cache.delete( self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id) ) def delete(self, blueprint, user=None, user_id=None): query = self.session.query(self.model).filter_by(provider=blueprint.name) uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) u = first( _get_real_user(ref, self.anon_user) for ref in (user, self.user, blueprint.config.get("user")) ) if self.user_required and not u and not uid: raise ValueError("Cannot delete OAuth token without an associated user") # check for user ID if hasattr(self.model, "user_id") and uid: query = query.filter_by(user_id=uid) # check for user (relationship property) elif hasattr(self.model, "user") and u: query = query.filter_by(user=u) # if we have the property, but not value, filter by None elif hasattr(self.model, "user_id"): query = query.filter_by(user_id=None) # run query query.delete() self.session.commit() # invalidate cache self.cache.delete( self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id) ) def _get_real_user(user, anon_user=None): """ Given a "user" that could be: * a real user object * a function that returns a real user object * a LocalProxy to a real user object (like Flask-Login's ``current_user``) This function returns the real user object, regardless of which we have. """ if hasattr(user, "_get_current_object"): # this is a proxy user = user._get_current_object() if callable(user): # this is a function user = user() if anon_user and isinstance(user, anon_user): return None return user flask-dance-7.1.0/flask_dance/contrib/000077500000000000000000000000001457161140100175615ustar00rootroot00000000000000flask-dance-7.1.0/flask_dance/contrib/README.rst000066400000000000000000000027561457161140100212620ustar00rootroot00000000000000Pre-set Configurations ====================== This directory contains pre-set consumer configurations for several popular OAuth providers. Have one that you'd like to add? Great, we love pull requests! However, you must meet certain criteria for your configuration to accepted: 1. You must create a Python file in this directory named after the provider: for example, ``my_provider.py``. 2. The file must declare a ``__maintainer__`` variable, with the name and email address of the maintainer of this configuration. 3. The file must have a factory function that returns an instance of OAuth1ConsumerBlueprint or an instance of OAuth2ConsumerBlueprint. The factory function must be named after the provider: for example, ``make_my_provider_blueprint()``. 4. The file must expose a variable named after the provider, which is a local proxy to the ``session`` attribute on the blueprint returned by the factory function. 5. You must create a Python file in the ``tests/contrib`` directory named after your provider with a prefix of ``test_``: for example, ``test_my_provider``. This file must contain tests for the file you created in this directory. 6. You must add your provider to the ``docs/providers.rst`` file, and ensure that your factory function has a RST-formatted docstring that the documentation can pick up. 7. All automated tests must pass, and the test coverage must not drop. 8. You must update the ``CHANGELOG.rst`` file to indicate that this provider was added. flask-dance-7.1.0/flask_dance/contrib/__init__.py000066400000000000000000000000001457161140100216600ustar00rootroot00000000000000flask-dance-7.1.0/flask_dance/contrib/atlassian.py000066400000000000000000000066421457161140100221220ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Przemyslaw Kanach " def make_atlassian_blueprint( client_id=None, client_secret=None, *, scope=None, reprompt_consent=False, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, ): """ Make a blueprint for authenticating with Atlassian using OAuth 2. This requires a client ID and client secret from Atlassian. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`ATLASSIAN_OAUTH_CLIENT_ID` and :envvar:`ATLASSIAN_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Atlassian. client_secret (str): The client secret for your application on Atlassian. scope (str, optional): comma-separated list of scopes for the OAuth token. reprompt_consent (bool): If True, force Atlassian to re-prompt the user for their consent, even if the user has already given their consent. Defaults to False. redirect_url (str): the URL to redirect to after the authentication dance is complete. redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for`. login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/atlassian``. authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/atlassian/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ authorization_url_params = {"audience": "api.atlassian.com"} if reprompt_consent: authorization_url_params["prompt"] = "consent" atlassian_bp = OAuth2ConsumerBlueprint( "atlassian", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://api.atlassian.com/", authorization_url="https://auth.atlassian.com/authorize", token_url="https://auth.atlassian.com/oauth/token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, authorization_url_params=authorization_url_params, session_class=session_class, storage=storage, ) atlassian_bp.from_config["client_id"] = "ATLASSIAN_OAUTH_CLIENT_ID" atlassian_bp.from_config["client_secret"] = "ATLASSIAN_OAUTH_CLIENT_SECRET" @atlassian_bp.before_app_request def set_applocal_session(): g.flask_dance_atlassian = atlassian_bp.session return atlassian_bp atlassian = LocalProxy(lambda: g.flask_dance_atlassian) flask-dance-7.1.0/flask_dance/contrib/authentiq.py000066400000000000000000000065761457161140100221530ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Pieter Ennes " def make_authentiq_blueprint( client_id=None, client_secret=None, *, scope="openid profile", redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, hostname="connect.authentiq.io", rule_kwargs=None, ): """ Make a blueprint for authenticating with authentiq using OAuth 2. This requires a client ID and client secret from authentiq. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`AUTHENTIQ_OAUTH_CLIENT_ID` and :envvar:`AUTHENTIQ_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Authentiq. client_secret (str): The client secret for your application on Authentiq. scope (str, optional): comma-separated list of scopes for the OAuth token. redirect_url (str): the URL to redirect to after the authentication dance is complete. redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for`. login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/authentiq``. authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/authentiq/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. hostname (str, optional): If using a private instance of authentiq CE/EE, specify the hostname, default is ``connect.authentiq.io``. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ authentiq_bp = OAuth2ConsumerBlueprint( "authentiq", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url=f"https://{hostname}/", authorization_url=f"https://{hostname}/authorize", token_url=f"https://{hostname}/token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) authentiq_bp.from_config["client_id"] = "AUTHENTIQ_OAUTH_CLIENT_ID" authentiq_bp.from_config["client_secret"] = "AUTHENTIQ_OAUTH_CLIENT_SECRET" @authentiq_bp.before_app_request def set_applocal_session(): g.flask_dance_authentiq = authentiq_bp.session return authentiq_bp authentiq = LocalProxy(lambda: g.flask_dance_authentiq) flask-dance-7.1.0/flask_dance/contrib/azure.py000066400000000000000000000130341457161140100212620ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Steven MARTINS " def make_azure_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, tenant="common", prompt=None, domain_hint=None, login_hint=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Azure AD using OAuth 2. This requires a client ID and client secret from Azure AD. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`AZURE_OAUTH_CLIENT_ID` and :envvar:`AZURE_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Azure AD. client_secret (str): The client secret for your application on Azure AD scope (str, optional): comma-separated list of scopes for the OAuth token. If the ``offline_access`` scope is included, automatic token refresh will be enabled. `See the Azure documentation for more information. `_ redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/azure`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/azure/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. tenant: Determine which accounts are allowed to authenticate with Azure. `See the Azure documentation for more information about this parameter. `_ Defaults to ``common``. prompt (str, optional): Indicate the type of user interaction that is required. Valid values are ``login``, ``select_account``, ``consent``, ``admin_consent``. Learn more about the options `here. `_ Defaults to ``None`` domain_hint (str, optional): Provides a hint about the tenant or domain that the user should use to sign in. The value of the domain_hint is a registered domain for the tenant. If the tenant is federated to an on-premises directory, AAD redirects to the specified tenant federation server. Defaults to ``None`` login_hint (str, optional): Can be used to pre-fill the username/email address field of the sign-in page for the user, if you know their username ahead of time. Often apps use this parameter during re-authentication, having already extracted the username from a previous sign-in using the preferred_username claim. Defaults to ``None`` rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ scope = scope or ["openid", "email", "profile", "User.Read"] token_url = f"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token" authorization_url = ( f"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize" ) authorization_url_params = {} if login_hint: authorization_url_params["login_hint"] = login_hint if domain_hint: authorization_url_params["domain_hint"] = domain_hint if prompt: authorization_url_params["prompt"] = prompt azure_bp = OAuth2ConsumerBlueprint( "azure", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://graph.microsoft.com", authorization_url=authorization_url, token_url=token_url, auto_refresh_url=token_url if "offline_access" in scope else None, redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, authorization_url_params=authorization_url_params, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) azure_bp.from_config["client_id"] = "AZURE_OAUTH_CLIENT_ID" azure_bp.from_config["client_secret"] = "AZURE_OAUTH_CLIENT_SECRET" @azure_bp.before_app_request def set_applocal_session(): g.flask_dance_azure = azure_bp.session return azure_bp azure = LocalProxy(lambda: g.flask_dance_azure) flask-dance-7.1.0/flask_dance/contrib/dexcom.py000066400000000000000000000063421457161140100214170ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Karan Bhatia " def make_dexcom_blueprint( client_id=None, client_secret=None, *, base_url="https://api.dexcom.com/", scope="offline_access", redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Dexcom using OAuth 2. This requires a client ID and client secret from Dexcom. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`DEXCOM_OAUTH_CLIENT_ID` and :envvar:`DEXCOM_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Dexcom. client_secret (str): The client secret for your application on Dexcom scope (str, optional): comma-separated list of scopes for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/dexcom`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/dexcom/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ dexcom_bp = OAuth2ConsumerBlueprint( "dexcom", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url=base_url, authorization_url="https://api.dexcom.com/v2/oauth2/login", token_url="https://api.dexcom.com/v2/oauth2/token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) dexcom_bp.from_config["client_id"] = "DEXCOM_OAUTH_CLIENT_ID" dexcom_bp.from_config["client_secret"] = "DEXCOM_OAUTH_CLIENT_SECRET" dexcom_bp.auto_refresh_url = dexcom_bp.token_url @dexcom_bp.before_app_request def set_applocal_session(): g.flask_dance_dexcom = dexcom_bp.session return dexcom_bp dexcom = LocalProxy(lambda: g.flask_dance_dexcom) flask-dance-7.1.0/flask_dance/contrib/digitalocean.py000066400000000000000000000065341457161140100225660ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Michael Abrahamsen " def make_digitalocean_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Digital Ocean using OAuth 2. This requires a client ID and client secret from Digital Ocean. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`DIGITALOCEAN_OAUTH_CLIENT_ID` and :envvar:`DIGITALOCEAN_OAUTH_CLIENT_SECRET`. Args: client_id (str): Client ID for your application on Digital Ocean client_secret (str): Client secret for your Digital Ocean application scope (str, optional): comma-separated list of scopes for the OAuth token. redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/digitalocean`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/digitalocean/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ digitalocean_bp = OAuth2ConsumerBlueprint( "digitalocean", __name__, client_id=client_id, client_secret=client_secret, scope=scope.replace(",", " ") if scope else None, base_url="https://cloud.digitalocean.com/v1/oauth", authorization_url="https://cloud.digitalocean.com/v1/oauth/authorize", token_url="https://cloud.digitalocean.com/v1/oauth/token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) digitalocean_bp.from_config["client_id"] = "DIGITALOCEAN_OAUTH_CLIENT_ID" digitalocean_bp.from_config["client_secret"] = "DIGITALOCEAN_OAUTH_CLIENT_SECRET" @digitalocean_bp.before_app_request def set_applocal_session(): g.flask_dance_digitalocean = digitalocean_bp.session return digitalocean_bp digitalocean = LocalProxy(lambda: g.flask_dance_digitalocean) flask-dance-7.1.0/flask_dance/contrib/discord.py000066400000000000000000000071241457161140100215660ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Michael Delpech " def make_discord_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, prompt="consent", rule_kwargs=None, ): """ Make a blueprint for authenticating with Discord using OAuth 2. This requires a client ID and client secret from Discord. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`DISCORD_OAUTH_CLIENT_ID` and :envvar:`DISCORD_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Discord. client_secret (str): The client secret for your application on Discord scope (list, optional): list of scopes (str) for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/discord`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/discord/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. prompt (str, optional): Define authorization flow. Defaults to ``consent``, setting it to ``None`` will skip user interaction if the application was previously approved. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ scope = scope or ["identify"] authorization_url_params = {"prompt": "consent"} if prompt is None: authorization_url_params["prompt"] = None discord_bp = OAuth2ConsumerBlueprint( "discord", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://discord.com/", token_url="https://discord.com/api/oauth2/token", authorization_url="https://discord.com/api/oauth2/authorize", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, authorization_url_params=authorization_url_params, rule_kwargs=rule_kwargs, ) discord_bp.from_config["client_id"] = "DISCORD_OAUTH_CLIENT_ID" discord_bp.from_config["client_secret"] = "DISCORD_OAUTH_CLIENT_SECRET" @discord_bp.before_app_request def set_applocal_session(): g.flask_dance_discord = discord_bp.session return discord_bp discord = LocalProxy(lambda: g.flask_dance_discord) flask-dance-7.1.0/flask_dance/contrib/dropbox.py000066400000000000000000000107231457161140100216130ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "David Baumgold " def make_dropbox_blueprint( app_key=None, app_secret=None, *, scope=None, offline=False, force_reapprove=False, disable_signup=False, require_role=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Dropbox using OAuth 2. This requires a client ID and client secret from Dropbox. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`DROPBOX_OAUTH_CLIENT_ID` and :envvar:`DROPBOX_OAUTH_CLIENT_SECRET`. For more information about the ``force_reapprove``, ``disable_signup``, and ``require_role`` arguments, `check the Dropbox API documentation `_. Args: app_key (str): The client ID for your application on Dropbox. app_secret (str): The client secret for your application on Dropbox scope (str, optional): Comma-separated list of scopes for the OAuth token offline (bool): Whether to request `Dropbox offline access `_ for the OAuth token. Defaults to False force_reapprove (bool): Force the user to approve the app again if they've already done so. disable_signup (bool): Prevent users from seeing a sign-up link on the authorization page. require_role (str): Pass the string ``work`` to require a Dropbox for Business account, or the string ``personal`` to require a personal account. redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/dropbox`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/dropbox/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ authorization_url_params = {} if offline: authorization_url_params["token_access_type"] = "offline" if force_reapprove: authorization_url_params["force_reapprove"] = "true" if disable_signup: authorization_url_params["disable_signup"] = "true" if require_role: authorization_url_params["require_role"] = require_role dropbox_bp = OAuth2ConsumerBlueprint( "dropbox", __name__, client_id=app_key, client_secret=app_secret, scope=scope, base_url="https://api.dropbox.com/2/", authorization_url="https://www.dropbox.com/oauth2/authorize", token_url="https://api.dropbox.com/oauth2/token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, authorization_url_params=authorization_url_params, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) dropbox_bp.from_config["client_id"] = "DROPBOX_OAUTH_CLIENT_ID" dropbox_bp.from_config["client_secret"] = "DROPBOX_OAUTH_CLIENT_SECRET" @dropbox_bp.before_app_request def set_applocal_session(): g.flask_dance_dropbox = dropbox_bp.session return dropbox_bp dropbox = LocalProxy(lambda: g.flask_dance_dropbox) flask-dance-7.1.0/flask_dance/contrib/facebook.py000066400000000000000000000070761457161140100217160ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Matt Bachmann " def make_facebook_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, rerequest_declined_permissions=False, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Facebook using OAuth 2. This requires a client ID and client secret from Facebook. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`FACEBOOK_OAUTH_CLIENT_ID` and :envvar:`FACEBOOK_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Facebook. client_secret (str): The client secret for your application on Facebook scope (str, optional): comma-separated list of scopes for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/facebook`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/facebook/authorized``. rerequest_declined_permissions (bool, optional): should the blueprint ask again for declined permissions. Defaults to ``False`` session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ authorization_url_params = {} if rerequest_declined_permissions: authorization_url_params["auth_type"] = "rerequest" facebook_bp = OAuth2ConsumerBlueprint( "facebook", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://graph.facebook.com/", authorization_url="https://www.facebook.com/dialog/oauth", authorization_url_params=authorization_url_params, token_url="https://graph.facebook.com/oauth/access_token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) facebook_bp.from_config["client_id"] = "FACEBOOK_OAUTH_CLIENT_ID" facebook_bp.from_config["client_secret"] = "FACEBOOK_OAUTH_CLIENT_SECRET" @facebook_bp.before_app_request def set_applocal_session(): g.flask_dance_facebook = facebook_bp.session return facebook_bp facebook = LocalProxy(lambda: g.flask_dance_facebook) flask-dance-7.1.0/flask_dance/contrib/fitbit.py000066400000000000000000000062751457161140100214260ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Karan Bhatia " def make_fitbit_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Fitbit using OAuth 2. This requires a client ID and client secret from Fitbit. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`FITBIT_OAUTH_CLIENT_ID` and :envvar:`FITBIT_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Fitbit. client_secret (str): The client secret for your application on Fitbit scope (str, optional): comma-separated list of scopes for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/fitbit`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/fitbit/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ fitbit_bp = OAuth2ConsumerBlueprint( "fitbit", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://api.fitbit.com/", authorization_url="https://www.fitbit.com/oauth2/authorize", token_url="https://api.fitbit.com/oauth2/token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) fitbit_bp.from_config["client_id"] = "FITBIT_OAUTH_CLIENT_ID" fitbit_bp.from_config["client_secret"] = "FITBIT_OAUTH_CLIENT_SECRET" fitbit_bp.auto_refresh_url = fitbit_bp.token_url @fitbit_bp.before_app_request def set_applocal_session(): g.flask_dance_fitbit = fitbit_bp.session return fitbit_bp fitbit = LocalProxy(lambda: g.flask_dance_fitbit) flask-dance-7.1.0/flask_dance/contrib/github.py000066400000000000000000000062241457161140100214210ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "David Baumgold " def make_github_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with GitHub using OAuth 2. This requires a client ID and client secret from GitHub. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`GITHUB_OAUTH_CLIENT_ID` and :envvar:`GITHUB_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on GitHub. client_secret (str): The client secret for your application on GitHub scope (str, optional): comma-separated list of scopes for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/github`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/github/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ github_bp = OAuth2ConsumerBlueprint( "github", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://api.github.com/", authorization_url="https://github.com/login/oauth/authorize", token_url="https://github.com/login/oauth/access_token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) github_bp.from_config["client_id"] = "GITHUB_OAUTH_CLIENT_ID" github_bp.from_config["client_secret"] = "GITHUB_OAUTH_CLIENT_SECRET" @github_bp.before_app_request def set_applocal_session(): g.flask_dance_github = github_bp.session return github_bp github = LocalProxy(lambda: g.flask_dance_github) flask-dance-7.1.0/flask_dance/contrib/gitlab.py000066400000000000000000000101641457161140100213770ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.requests import OAuth2Session __maintainer__ = "Justin Georgeson " class NoVerifyOAuth2Session(OAuth2Session): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.verify = False def make_gitlab_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, hostname="gitlab.com", verify_tls_certificates=True, rule_kwargs=None, ): """ Make a blueprint for authenticating with GitLab using OAuth 2. This requires a client ID and client secret from GitLab. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`GITLAB_OAUTH_CLIENT_ID` and :envvar:`GITLAB_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on GitLab. client_secret (str): The client secret for your application on GitLab scope (str, optional): comma-separated list of scopes for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/gitlab`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/gitlab/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. hostname (str, optional): If using a private instance of GitLab CE/EE, specify the hostname, default is ``gitlab.com``. verify_tls_certificates (bool, optional): Specify whether TLS certificates should be verified. Set this to ``False`` if certificates fail to validate for self-hosted GitLab instances. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. specify the hostname, default is ``gitlab.com`` :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ if not verify_tls_certificates: if session_class: raise ValueError( "cannot override session_class and disable certificate validation" ) else: session_class = NoVerifyOAuth2Session gitlab_bp = OAuth2ConsumerBlueprint( "gitlab", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url=f"https://{hostname}/api/v4/", authorization_url=f"https://{hostname}/oauth/authorize", token_url=f"https://{hostname}/oauth/token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, token_url_params={"verify": verify_tls_certificates}, rule_kwargs=rule_kwargs, ) gitlab_bp.from_config["client_id"] = "GITLAB_OAUTH_CLIENT_ID" gitlab_bp.from_config["client_secret"] = "GITLAB_OAUTH_CLIENT_SECRET" @gitlab_bp.before_app_request def set_applocal_session(): g.flask_dance_gitlab = gitlab_bp.session return gitlab_bp gitlab = LocalProxy(lambda: g.flask_dance_gitlab) flask-dance-7.1.0/flask_dance/contrib/google.py000066400000000000000000000123701457161140100214120ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "David Baumgold " def make_google_blueprint( client_id=None, client_secret=None, *, scope=None, offline=False, reprompt_consent=False, reprompt_select_account=False, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, hosted_domain=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Google using OAuth 2. This requires a client ID and client secret from Google. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`GOOGLE_OAUTH_CLIENT_ID` and :envvar:`GOOGLE_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Google client_secret (str): The client secret for your application on Google scope (str, optional): comma-separated list of scopes for the OAuth token. Defaults to the "https://www.googleapis.com/auth/userinfo.profile" scope. offline (bool): Whether to request `offline access `_ for the OAuth token. Defaults to False reprompt_consent (bool): If True, force Google to re-prompt the user for their consent, even if the user has already given their consent. Defaults to False reprompt_select_account (bool): If True, force Google to re-prompt the select account page, even if there is a single logged-in user. Defaults to False redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/google`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/google/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. hosted_domain (str, optional): The domain of the G Suite user. Used to indicate that the account selection UI should be optimized for accounts at this domain. Note that this only provides UI optimization, and requires response validation (see warning). rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. .. _google_hosted_domain_warning: .. warning:: The ``hosted_domain`` argument **only provides UI optimization**. Don't rely on this argument to control who can access your application. You must verify that the ``hd`` claim of the response ID token matches the ``hosted_domain`` argument passed to ``make_google_blueprint``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ scope = scope or ["https://www.googleapis.com/auth/userinfo.profile"] authorization_url_params = {} prompt_params = [] auto_refresh_url = None if offline: authorization_url_params["access_type"] = "offline" auto_refresh_url = "https://accounts.google.com/o/oauth2/token" if reprompt_consent: prompt_params.append("consent") if reprompt_select_account: prompt_params.append("select_account") if prompt_params: prompt_params = " ".join(prompt_params) authorization_url_params["prompt"] = prompt_params if hosted_domain: authorization_url_params["hd"] = hosted_domain google_bp = OAuth2ConsumerBlueprint( "google", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://www.googleapis.com/", authorization_url="https://accounts.google.com/o/oauth2/auth", token_url="https://accounts.google.com/o/oauth2/token", auto_refresh_url=auto_refresh_url, redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, authorization_url_params=authorization_url_params, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) google_bp.from_config["client_id"] = "GOOGLE_OAUTH_CLIENT_ID" google_bp.from_config["client_secret"] = "GOOGLE_OAUTH_CLIENT_SECRET" @google_bp.before_app_request def set_applocal_session(): g.flask_dance_google = google_bp.session return google_bp google = LocalProxy(lambda: g.flask_dance_google) flask-dance-7.1.0/flask_dance/contrib/heroku.py000066400000000000000000000071671457161140100214430ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.requests import OAuth2Session __maintainer__ = "David Baumgold " class HerokuOAuth2Session(OAuth2Session): def __init__(self, api_version, *args, **kwargs): super().__init__(*args, **kwargs) accept = f"application/vnd.heroku+json; version={api_version}" self.headers["Accept"] = accept def make_heroku_blueprint( client_id=None, client_secret=None, *, scope=None, api_version="3", redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Heroku using OAuth 2. This requires a client ID and client secret from Heroku. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`HEROKU_OAUTH_CLIENT_ID` and :envvar:`HEROKU_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Heroku. client_secret (str): The client secret for your application on Heroku scope (str, optional): comma-separated list of scopes for the OAuth token api_version (str): The version number of the Heroku API you want to use. Defaults to version 3. redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/heroku`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/heroku/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.contrib.HerokuOAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ heroku_bp = OAuth2ConsumerBlueprint( "heroku", __name__, client_id=client_id, client_secret=client_secret, scope=scope, api_version=api_version, base_url="https://api.heroku.com/", authorization_url="https://id.heroku.com/oauth/authorize", token_url="https://id.heroku.com/oauth/token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class or HerokuOAuth2Session, storage=storage, rule_kwargs=rule_kwargs, ) heroku_bp.from_config["client_id"] = "HEROKU_OAUTH_CLIENT_ID" heroku_bp.from_config["client_secret"] = "HEROKU_OAUTH_CLIENT_SECRET" @heroku_bp.before_app_request def set_applocal_session(): g.flask_dance_heroku = heroku_bp.session return heroku_bp heroku = LocalProxy(lambda: g.flask_dance_heroku) flask-dance-7.1.0/flask_dance/contrib/jira.py000066400000000000000000000076541457161140100210740ustar00rootroot00000000000000import os.path from flask import g from oauthlib.oauth1 import SIGNATURE_RSA from urlobject import URLObject from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth1ConsumerBlueprint from flask_dance.consumer.requests import OAuth1Session __maintainer__ = "David Baumgold " class JsonOAuth1Session(OAuth1Session): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.headers["Content-Type"] = "application/json" def make_jira_blueprint( base_url, consumer_key=None, rsa_key=None, *, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with JIRA using OAuth 1. This requires a consumer key and RSA key for the JIRA application link. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`JIRA_OAUTH_CONSUMER_KEY` and :envvar:`JIRA_OAUTH_RSA_KEY`. Args: base_url (str): The base URL of your JIRA installation. For example, for Atlassian's hosted Cloud JIRA, the base_url would be ``https://jira.atlassian.com`` consumer_key (str): The consumer key for your Application Link on JIRA rsa_key (str or path): The RSA private key for your Application Link on JIRA. This can be the contents of the key as a string, or a path to the key file on disk. redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/jira`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/jira/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.contrib.jira.JsonOAuth1Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth1ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ if rsa_key and os.path.isfile(rsa_key): with open(rsa_key) as f: rsa_key = f.read() base_url = URLObject(base_url) jira_bp = OAuth1ConsumerBlueprint( "jira", __name__, client_key=consumer_key, rsa_key=rsa_key, signature_method=SIGNATURE_RSA, base_url=base_url, request_token_url=base_url.relative("plugins/servlet/oauth/request-token"), access_token_url=base_url.relative("plugins/servlet/oauth/access-token"), authorization_url=base_url.relative("plugins/servlet/oauth/authorize"), redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class or JsonOAuth1Session, storage=storage, rule_kwargs=rule_kwargs, ) jira_bp.from_config["client_key"] = "JIRA_OAUTH_CONSUMER_KEY" jira_bp.from_config["rsa_key"] = "JIRA_OAUTH_RSA_KEY" @jira_bp.before_app_request def set_applocal_session(): g.flask_dance_jira = jira_bp.session return jira_bp jira = LocalProxy(lambda: g.flask_dance_jira) flask-dance-7.1.0/flask_dance/contrib/linkedin.py000066400000000000000000000064021457161140100217320ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "David Baumgold " def make_linkedin_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with LinkedIn using OAuth 2. This requires a client ID and client secret from LinkedIn. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`LINKEDIN_OAUTH_CLIENT_ID` and :envvar:`LINKEDIN_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on LinkedIn. client_secret (str): The client secret for your application on LinkedIn scope (str, optional): comma-separated list of scopes for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/linkedin`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/linkedin/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ linkedin_bp = OAuth2ConsumerBlueprint( "linkedin", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://api.linkedin.com/v2/", authorization_url="https://www.linkedin.com/oauth/v2/authorization", token_url="https://www.linkedin.com/oauth/v2/accessToken", token_url_params={"include_client_id": True}, redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) linkedin_bp.from_config["client_id"] = "LINKEDIN_OAUTH_CLIENT_ID" linkedin_bp.from_config["client_secret"] = "LINKEDIN_OAUTH_CLIENT_SECRET" @linkedin_bp.before_app_request def set_applocal_session(): g.flask_dance_linkedin = linkedin_bp.session return linkedin_bp linkedin = LocalProxy(lambda: g.flask_dance_linkedin) flask-dance-7.1.0/flask_dance/contrib/meetup.py000066400000000000000000000063241457161140100214370ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "David Baumgold " def make_meetup_blueprint( key=None, secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Meetup using OAuth 2. This requires an OAuth consumer from Meetup. You should either pass the key and secret to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`MEETUP_OAUTH_CLIENT_ID` and :envvar:`MEETUP_OAUTH_CLIENT_SECRET`. Args: key (str): The OAuth consumer key for your application on Meetup secret (str): The OAuth consumer secret for your application on Meetup scope (str, optional): comma-separated list of scopes for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/meetup`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/meetup/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ scope = scope or ["basic"] meetup_bp = OAuth2ConsumerBlueprint( "meetup", __name__, client_id=key, client_secret=secret, scope=scope, base_url="https://api.meetup.com/2/", authorization_url="https://secure.meetup.com/oauth2/authorize", token_url="https://secure.meetup.com/oauth2/access", token_url_params={"include_client_id": True}, redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) meetup_bp.from_config["client_id"] = "MEETUP_OAUTH_CLIENT_ID" meetup_bp.from_config["client_secret"] = "MEETUP_OAUTH_CLIENT_SECRET" @meetup_bp.before_app_request def set_applocal_session(): g.flask_dance_meetup = meetup_bp.session return meetup_bp meetup = LocalProxy(lambda: g.flask_dance_meetup) flask-dance-7.1.0/flask_dance/contrib/nylas.py000066400000000000000000000063401457161140100212640ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "David Baumgold " def make_nylas_blueprint( client_id=None, client_secret=None, *, scope="email", redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Nylas using OAuth 2. This requires an API ID and API secret from Nylas. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`NYLAS_OAUTH_CLIENT_ID` and :envvar:`NYLAS_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your developer account on Nylas. client_secret (str): The client secret for your developer account on Nylas. scope (str, optional): comma-separated list of scopes for the OAuth token. Defaults to "email". redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/nylas`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/nylas/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ nylas_bp = OAuth2ConsumerBlueprint( "nylas", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://api.nylas.com/", authorization_url="https://api.nylas.com/oauth/authorize", token_url="https://api.nylas.com/oauth/token", token_url_params={"include_client_id": True}, redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) nylas_bp.from_config["client_id"] = "NYLAS_OAUTH_CLIENT_ID" nylas_bp.from_config["client_secret"] = "NYLAS_OAUTH_CLIENT_SECRET" @nylas_bp.before_app_request def set_applocal_session(): g.flask_dance_nylas = nylas_bp.session return nylas_bp nylas = LocalProxy(lambda: g.flask_dance_nylas) flask-dance-7.1.0/flask_dance/contrib/orcid.py000066400000000000000000000071761457161140100212460ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Matthew Evans " def make_orcid_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, sandbox=False, ): """ Make a blueprint for authenticating with ORCID (https://orcid.org) using OAuth2. This requires a client ID and client secret from ORCID. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`ORCID_OAUTH_CLIENT_ID` and :envvar:`ORCID_OAUTH_CLIENT_SECRET`. The ORCID Sandbox API (https://sandbox.orcid.org) will be used if the ``sandbox`` argument is set to true. Args: client_id (str): The client ID for your application on ORCID. client_secret (str): The client secret for your application on ORCID scope (str, optional): comma-separated list of scopes for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/orcid`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/orcid/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. sandbox (bool): Whether to use the ORCID sandbox instead of the production API. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ base_url = "https://api.orcid.org" authorization_url = "https://orcid.org/oauth/authorize" token_url = "https://orcid.org/oauth/token" if sandbox: base_url = "https://api.sandbox.orcid.org" authorization_url = "https://sandbox.orcid.org/oauth/authorize" token_url = "https://sandbox.orcid.org/oauth/token" orcid_bp = OAuth2ConsumerBlueprint( "orcid", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url=base_url, authorization_url=authorization_url, token_url=token_url, redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) orcid_bp.from_config["client_id"] = "ORCID_OAUTH_CLIENT_ID" orcid_bp.from_config["client_secret"] = "ORCID_OAUTH_CLIENT_SECRET" @orcid_bp.before_app_request def set_applocal_session(): g.flask_dance_orcid = orcid_bp.session return orcid_bp orcid = LocalProxy(lambda: g.flask_dance_orcid) flask-dance-7.1.0/flask_dance/contrib/osm.py000066400000000000000000000062151457161140100207350ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Martijn van Exel " def make_osm_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with OpenStreetMap (OSM) using OAuth 2. This requires a client ID and client secret from OpenStreetMap. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`OSM_OAUTH_CLIENT_ID` and :envvar:`OSM_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on OpenStreetMap. client_secret (str): The client secret for your application on OpenStreetMap scope (str, optional): comma-separated list of scopes for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/osm`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/osm/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ osm_bp = OAuth2ConsumerBlueprint( "osm", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://www.openstreetmap.org/api/0.6/", authorization_url="https://www.openstreetmap.org/oauth2/authorize", token_url="https://www.openstreetmap.org/oauth2/token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) osm_bp.from_config["client_id"] = "OSM_OAUTH_CLIENT_ID" osm_bp.from_config["client_secret"] = "OSM_OAUTH_CLIENT_SECRET" @osm_bp.before_app_request def set_applocal_session(): g.flask_dance_osm = osm_bp.session return osm_bp osm = LocalProxy(lambda: g.flask_dance_osm) flask-dance-7.1.0/flask_dance/contrib/reddit.py000066400000000000000000000105751457161140100214160ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance import __version__ as _flask_dance_version from flask_dance.consumer import OAuth2ConsumerBlueprint, OAuth2Session __maintainer__ = "Sergey Storchay " DEFAULT_USER_AGENT = f"Flask-Dance/{_flask_dance_version}" class RedditOAuth2Session(OAuth2Session): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # The Reddit API requires a non-generic user agent self.headers["User-Agent"] = self.blueprint.user_agent or DEFAULT_USER_AGENT def fetch_token(self, *args, **kwargs): # Pass client_id to session so it could trigger Basic Auth return super().fetch_token(client_id=self.blueprint.client_id, *args, **kwargs) def make_reddit_blueprint( client_id=None, client_secret=None, *, scope="identity", permanent=False, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, user_agent=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Reddit using OAuth 2. This requires a client ID and client secret from Reddit. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`REDDIT_OAUTH_CLIENT_ID` and :envvar:`REDDIT_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Reddit. client_secret (str): The client secret for your application on Reddit scope (str, optional): space-separated list of scopes for the OAuth token Defaults to ``identity`` permanent (bool, optional): Whether to request permanent access token. Defaults to False, access will be valid for 1 hour redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/reddit`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/reddit/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.contrib.reddit.RedditOAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. user_agent (str, optional): User agent for the requests to Reddit API. Defaults to ``Flask-Dance/{{version}}``. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ authorization_url_params = {} if permanent: authorization_url_params["duration"] = "permanent" reddit_bp = OAuth2ConsumerBlueprint( "reddit", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://oauth.reddit.com/", authorization_url="https://www.reddit.com/api/v1/authorize", authorization_url_params=authorization_url_params, token_url="https://www.reddit.com/api/v1/access_token", auto_refresh_url="https://www.reddit.com/api/v1/access_token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class or RedditOAuth2Session, storage=storage, rule_kwargs=rule_kwargs, ) reddit_bp.from_config["client_id"] = "REDDIT_OAUTH_CLIENT_ID" reddit_bp.from_config["client_secret"] = "REDDIT_OAUTH_CLIENT_SECRET" reddit_bp.user_agent = user_agent @reddit_bp.before_app_request def set_applocal_session(): g.flask_dance_reddit = reddit_bp.session return reddit_bp reddit = LocalProxy(lambda: g.flask_dance_reddit) flask-dance-7.1.0/flask_dance/contrib/salesforce.py000066400000000000000000000076251457161140100222730ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Przemyslaw Kanach " def make_salesforce_blueprint( client_id=None, client_secret=None, *, scope=None, reprompt_consent=False, hostname=None, is_sandbox=False, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, ): """ Make a blueprint for authenticating with Salesforce using OAuth 2. This requires a client ID and client secret from Salesforce. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`SALESFORCE_OAUTH_CLIENT_ID` and :envvar:`SALESFORCE_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Salesforce. client_secret (str): The client secret for your application on Salesforce. scope (str, optional): comma-separated list of scopes for the OAuth token. reprompt_consent (bool): If True, force Salesforce to re-prompt the user for their consent, even if the user has already given their consent. Defaults to False. hostname (str, optional): The hostname of your Salesforce instance. By default, Salesforce uses ``login.salesforce.com`` for production instances and ``test.salesforce.com`` for sandboxes. is_sandbox (bool): If hostname is not defined specify whether Salesforce instance is a sandbox. Defaults to False. redirect_url (str): the URL to redirect to after the authentication dance is complete. redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for`. login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/salesforce``. authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/salesforce/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ authorization_url_params = {} if reprompt_consent: authorization_url_params["prompt"] = "consent" if not hostname: hostname = "test.salesforce.com" if is_sandbox else "login.salesforce.com" salesforce_bp = OAuth2ConsumerBlueprint( "salesforce", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url=f"https://{hostname}/", authorization_url=f"https://{hostname}/services/oauth2/authorize", token_url=f"https://{hostname}/services/oauth2/token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, authorization_url_params=authorization_url_params, session_class=session_class, storage=storage, ) salesforce_bp.from_config["client_id"] = "SALESFORCE_OAUTH_CLIENT_ID" salesforce_bp.from_config["client_secret"] = "SALESFORCE_OAUTH_CLIENT_SECRET" @salesforce_bp.before_app_request def set_applocal_session(): g.flask_dance_salesforce = salesforce_bp.session return salesforce_bp salesforce = LocalProxy(lambda: g.flask_dance_salesforce) flask-dance-7.1.0/flask_dance/contrib/slack.py000066400000000000000000000072341457161140100212360ustar00rootroot00000000000000from flask import g from requests_oauthlib.compliance_fixes.slack import slack_compliance_fix from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "David Baumgold " class SlackBlueprint(OAuth2ConsumerBlueprint): def session_created(self, session): return slack_compliance_fix(session) def make_slack_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, subdomain=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Slack using OAuth 2. This requires a client ID and client secret from Slack. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`SLACK_OAUTH_CLIENT_ID` and :envvar:`SLACK_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Slack. client_secret (str): The client secret for your application on Slack scope (str, optional): comma-separated list of scopes for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/slack`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/slack/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. subdomain (str, optional): the name of the subdomain under which your Slack space is accessed. Providing this may improve the login experience. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ scope = scope or ["identify", "chat:write:bot"] slack_bp = SlackBlueprint( "slack", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://slack.com/api/", authorization_url=( "https://slack.com/oauth/authorize" if subdomain is None else f"https://{subdomain}.slack.com/oauth/authorize" ), token_url="https://slack.com/api/oauth.access", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) slack_bp.from_config["client_id"] = "SLACK_OAUTH_CLIENT_ID" slack_bp.from_config["client_secret"] = "SLACK_OAUTH_CLIENT_SECRET" @slack_bp.before_app_request def set_applocal_session(): g.flask_dance_slack = slack_bp.session return slack_bp slack = LocalProxy(lambda: g.flask_dance_slack) flask-dance-7.1.0/flask_dance/contrib/spotify.py000066400000000000000000000062351457161140100216360ustar00rootroot00000000000000from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Nick DiRienzo " def make_spotify_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Spotify using OAuth 2. This requires a client ID and client secret from Spotify. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`SPOTIFY_OAUTH_CLIENT_ID` and :envvar:`SPOTIFY_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Spotify. client_secret (str): The client secret for your application on Spotify scope (str, optional): comma-separated list of scopes for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/spotify`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/spotify/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ spotify_bp = OAuth2ConsumerBlueprint( "spotify", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://api.spotify.com", authorization_url="https://accounts.spotify.com/authorize", token_url="https://accounts.spotify.com/api/token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) spotify_bp.from_config["client_id"] = "SPOTIFY_OAUTH_CLIENT_ID" spotify_bp.from_config["client_secret"] = "SPOTIFY_OAUTH_CLIENT_SECRET" @spotify_bp.before_app_request def set_applocal_session(): g.flask_dance_spotify = spotify_bp.session return spotify_bp spotify = LocalProxy(lambda: g.flask_dance_spotify) flask-dance-7.1.0/flask_dance/contrib/strava.py000066400000000000000000000077101457161140100214400ustar00rootroot00000000000000from flask import g, request from werkzeug.local import LocalProxy from flask_dance import __version__ as _flask_dance_version from flask_dance.consumer import OAuth2ConsumerBlueprint, OAuth2Session __maintainer__ = "Jimmy Hedman " DEFAULT_USER_AGENT = f"Flask-Dance/{_flask_dance_version}" class StravaOAuth2Session(OAuth2Session): def fetch_token(self, *args, **kwargs): # Pass client_id to session so it could trigger Basic Auth return super().fetch_token( include_client_id=True, method="POST", code=request.args.get("code"), *args, **kwargs, ) def make_strava_blueprint( client_id=None, client_secret=None, *, scope="read", redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=None, storage=None, user_agent=None, rule_kwargs=None, ): """ Make a blueprint for authenticating with Strava using OAuth 2. This requires a client ID and client secret from Strava. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`STRAVA_OAUTH_CLIENT_ID` and :envvar:`STRAVA_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Strava. client_secret (str): The client secret for your application on Strava scope (str, optional): space-separated list of scopes for the OAuth token Defaults to ``identity`` redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/strava`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/strava/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.contrib.strava.StravaOAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. user_agent (str, optional): User agent for the requests to Strava API. Defaults to ``Flask-Dance/{{version}}``. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ strava_bp = OAuth2ConsumerBlueprint( "strava", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://www.strava.com/api/v3", authorization_url="https://www.strava.com/api/v3/oauth/authorize", token_url="https://www.strava.com/api/v3/oauth/token", auto_refresh_url="https://www.strava.com/api/v3/oauth/token", redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class or StravaOAuth2Session, storage=storage, rule_kwargs=rule_kwargs, ) strava_bp.from_config["client_id"] = "STRAVA_OAUTH_CLIENT_ID" strava_bp.from_config["client_secret"] = "STRAVA_OAUTH_CLIENT_SECRET" strava_bp.user_agent = user_agent @strava_bp.before_app_request def set_applocal_session(): g.flask_dance_strava = strava_bp.session return strava_bp strava = LocalProxy(lambda: g.flask_dance_strava) flask-dance-7.1.0/flask_dance/contrib/twitch.py000066400000000000000000000101371457161140100214370ustar00rootroot00000000000000"""Blueprint for connecting to Twitch API.""" from flask import g from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.requests import OAuth2Session __maintainer__ = "Kerry Hatcher " class ClientIdHeaderOAuth2Session(OAuth2Session): """ https://blog.twitch.tv/en/2016/05/05/client-id-required-for-kraken-api-calls-afbb8e95f843/ """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.headers["client-id"] = self.client_id def make_twitch_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, redirect_to=None, login_url=None, authorized_url=None, session_class=ClientIdHeaderOAuth2Session, storage=None, rule_kwargs=None, ): """Make a blueprint for authenticating with Twitch using OAuth 2. This requires a client ID and client secret from Twitch. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`TWITCH_OAUTH_CLIENT_ID` and :envvar:`TWITCH_OAUTH_CLIENT_SECRET`. Args: client_id (str): The client ID for your application on Twitch. Defaults to app config "TWITCH_OAUTH_CLIENT_ID". client_secret (str): The client Secret for your application on Twitch. Defaults to app config "TWITCH_OAUTH_CLIENT_SECRET". scope (list, optional): Comma-separated list of scopes for the OAuth token. Defaults to None. redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/twitch`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/twitch/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.contrib.twitch.ClientIdHeaderOAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. Returns: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` A :doc:`blueprint ` to attach to your Flask app. """ twitch_bp = OAuth2ConsumerBlueprint( "twitch", __name__, client_id=client_id, client_secret=client_secret, scope=scope, base_url="https://api.twitch.tv/helix/", authorization_url="https://id.twitch.tv/oauth2/authorize", token_url="https://id.twitch.tv/oauth2/token", token_url_params={"include_client_id": True}, redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, authorized_url=authorized_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) twitch_bp.from_config["client_id"] = "TWITCH_OAUTH_CLIENT_ID" twitch_bp.from_config["client_secret"] = "TWITCH_OAUTH_CLIENT_SECRET" # TODO: The key won't auto renew. See https://github.com/singingwolfboy/flask-dance/issues/35 # I think this will work but needs a test. twitch_bp.auto_refresh_url = twitch_bp.token_url twitch_bp.auto_refresh_kwargs = { "client_id": twitch_bp.client_id, "client_secret": twitch_bp.client_secret, } @twitch_bp.before_app_request def set_applocal_session(): g.flask_dance_twitch = twitch_bp.session return twitch_bp twitch = LocalProxy(lambda: g.flask_dance_twitch) flask-dance-7.1.0/flask_dance/contrib/zoho.py000066400000000000000000000122161457161140100211140ustar00rootroot00000000000000from flask import g from oauthlib.oauth2.rfc6749.clients.web_application import WebApplicationClient from werkzeug.local import LocalProxy from flask_dance.consumer import OAuth2ConsumerBlueprint __maintainer__ = "Ryan Schaffer " AUTH_HEADER = "auth_header" URI_QUERY = "query" BODY = "body" ZOHO_TOKEN_HEADER = "Zoho-oauthtoken" def make_zoho_blueprint( client_id=None, client_secret=None, *, scope=None, redirect_url=None, offline=False, redirect_to=None, login_url=None, session_class=None, storage=None, reprompt_consent=False, rule_kwargs=None, ): """ Make a blueprint for authenticating with Zoho using OAuth 2. This requires a client ID and client secret from Zoho. You should either pass them to this constructor, or make sure that your Flask application config defines them, using the variables :envvar:`ZOHO_OAUTH_CLIENT_ID` and :envvar:`ZOHO_OAUTH_CLIENT_SECRET`. IMPORTANT: Configuring the base_url is not supported in this config. Args: client_id (str): The client ID for your application on Zoho. client_secret (str): The client secret for your application on Zoho scope (list, optional): list of scopes (str) for the OAuth token redirect_url (str): the URL to redirect to after the authentication dance is complete redirect_to (str): if ``redirect_url`` is not defined, the name of the view to redirect to after the authentication dance is complete. The actual URL will be determined by :func:`flask.url_for` login_url (str, optional): the URL path for the ``login`` view. Defaults to ``/zoho`` authorized_url (str, optional): the URL path for the ``authorized`` view. Defaults to ``/zoho/authorized``. session_class (class, optional): The class to use for creating a Requests session. Defaults to :class:`~flask_dance.consumer.requests.OAuth2Session`. storage: A token storage class, or an instance of a token storage class, to use for this blueprint. Defaults to :class:`~flask_dance.consumer.storage.session.SessionStorage`. offline (bool): Whether to request `offline access` for the OAuth token. Defaults to False reprompt_consent (bool): If True, force Zoho to re-prompt the user for their consent, even if the user has already given their consent. Defaults to False. rule_kwargs (dict, optional): Additional arguments that should be passed when adding the login and authorized routes. Defaults to ``None``. :rtype: :class:`~flask_dance.consumer.OAuth2ConsumerBlueprint` :returns: A :doc:`blueprint ` to attach to your Flask app. """ scope = scope or ["ZohoCRM.users.all"] base_url = "https://www.zohoapis.com/" client = ZohoWebClient(client_id, token_type=ZOHO_TOKEN_HEADER) authorization_url_params = {} authorization_url_params["access_type"] = "offline" if offline else "online" if reprompt_consent: authorization_url_params["prompt"] = "consent" zoho_bp = OAuth2ConsumerBlueprint( "zoho", __name__, client_id=client_id, client_secret=client_secret, client=client, scope=scope, base_url=base_url, token_url="https://accounts.zoho.com/oauth/v2/token", authorization_url="https://accounts.zoho.com/oauth/v2/auth", authorization_url_params=authorization_url_params, redirect_url=redirect_url, redirect_to=redirect_to, login_url=login_url, session_class=session_class, storage=storage, rule_kwargs=rule_kwargs, ) zoho_bp.from_config["client_id"] = "ZOHO_OAUTH_CLIENT_ID" zoho_bp.from_config["client_secret"] = "ZOHO_OAUTH_CLIENT_SECRET" @zoho_bp.before_app_request def set_applocal_session(): g.flask_dance_zoho = zoho_bp.session return zoho_bp zoho = LocalProxy(lambda: g.flask_dance_zoho) class ZohoWebClient(WebApplicationClient): """ Remove the requirement that token_types adhere to OAuth Standard """ @property def token_types(self): return { "Bearer": self._add_bearer_token, "MAC": self._add_mac_token, ZOHO_TOKEN_HEADER: self._add_zoho_token, } def _add_zoho_token( self, uri, http_method="GET", body=None, headers=None, token_placement=None ): """Add a zoho token to the request uri, body or authorization header. follows bearer pattern""" headers = self.prepare_zoho_headers(self.access_token, headers) return uri, headers, body @staticmethod def prepare_zoho_headers(token, headers=None): """Add a `Zoho Token`_ to the request URI. Recommended method of passing bearer tokens. Authorization: Zoho-oauthtoken h480djs93hd8 .. _`Zoho-oauthtoken Token`: custom zoho token """ headers = headers or {} headers["Authorization"] = "{token_header} {token}".format( token_header=ZOHO_TOKEN_HEADER, token=token ) return headers flask-dance-7.1.0/flask_dance/fixtures/000077500000000000000000000000001457161140100177725ustar00rootroot00000000000000flask-dance-7.1.0/flask_dance/fixtures/__init__.py000066400000000000000000000000001457161140100220710ustar00rootroot00000000000000flask-dance-7.1.0/flask_dance/fixtures/pytest.py000066400000000000000000000056411457161140100217020ustar00rootroot00000000000000""" Flask-Dance provides a handy Pytest_ fixture named ``betamax_record_flask_dance`` that wraps Flask-Dance sessions with Betamax_ to record and replay HTTP requests. In order to use this fixture, you must install Betamax in your testing environment. You must also define two other Pytest fixtures: ``app`` and ``flask_dance_sessions``. The ``app`` fixture must return the Flask app that is being tested, and the ``flask_dance_sessions`` fixture must return the Flask-Dance session or sessions that should be wrapped using Betamax. For example: .. code-block:: python from flask_dance.contrib.github import github from myapp import app as _app @pytest.fixture def app(): return _app @pytest.fixture def flask_dance_sessions(): return github The ``flask_dance_sessions`` fixture can return either a single session, or a list/tuple of sessions. To use this fixture, it's generally easiest to decorate your test with :func:`pytest.mark.usefixtures`, like this: .. code-block:: python :emphasize-lines: 1 @pytest.mark.usefixtures("betamax_record_flask_dance") def test_home_page(app): with app.test_client() as client: response = client.get("/", base_url="https://example.com") assert response.status_code == 200 """ import pytest try: from betamax import Betamax except ImportError: Betamax = None @pytest.fixture def betamax_record_flask_dance(app, flask_dance_sessions, request): """ Wraps the specified Flask-Dance sessions with Betamax This allows you to record and re-play HTTP requests from Flask-Dance sessions. Requires the Betamax library. You must also define a `flask_dance_sessions` fixture, that defines the session or sessions that should be wrapped with Betamax. """ if not Betamax: raise ImportError( "The `betamax_record_flask_dance` fixture depends on " "the `betamax` module" ) if isinstance(flask_dance_sessions, (list, tuple)): betamax_setup_info = [ ( session, f"{request.node.name}-{index}", ) for index, session in enumerate(flask_dance_sessions) ] else: session = flask_dance_sessions betamax_setup_info = [(session, request.node.name)] recorders = [] @app.before_request def wrap_flask_dance_with_betamax(): for session, cassette_name in betamax_setup_info: recorder = Betamax(session).use_cassette(cassette_name) recorders.append(recorder) recorder.start() request.addfinalizer( lambda: app.before_request_funcs[None].remove(wrap_flask_dance_with_betamax) ) @app.after_request def unwrap(response): for recorder in recorders: recorder.stop() return response request.addfinalizer(lambda: app.after_request_funcs[None].remove(unwrap)) return app flask-dance-7.1.0/flask_dance/utils.py000066400000000000000000000020331457161140100176310ustar00rootroot00000000000000import functools class FakeCache: """ An object that mimics just enough of Flask-Caching's API to be compatible with our needs, but does nothing. """ def get(self, key): return None def set(self, key, value): return None def delete(self, key): return None def first(iterable, default=None, key=None): """ Return the first truthy value of an iterable. Shamelessly stolen from https://github.com/hynek/first """ if key is None: for el in iterable: if el: return el else: for el in iterable: if key(el): return el return default sentinel = object() def getattrd(obj, name, default=sentinel): """ Same as getattr(), but allows dot notation lookup Source: http://stackoverflow.com/a/14324459 """ try: return functools.reduce(getattr, name.split("."), obj) except AttributeError as e: if default is not sentinel: return default raise flask-dance-7.1.0/pyproject.toml000066400000000000000000000041141457161140100166030ustar00rootroot00000000000000[build-system] requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" [project] name = "Flask-Dance" authors = [{ name = "David Baumgold", email = "david@davidbaumgold.com" }] maintainers = [{ name = "Daniele Sluijters" }] readme = "README.rst" dynamic = ["description", "version"] license = { file = "LICENSE" } requires-python = ">=3.6" classifiers = [ "License :: OSI Approved :: MIT License", "Framework :: Flask", "Framework :: Pytest", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ] dependencies = [ "requests>=2.0", "oauthlib>=3.2", "requests-oauthlib>=1.0.0", "Flask>=2.0.3", "Werkzeug", "urlobject", ] [project.urls] Documentation = "https://flask-dance.readthedocs.io/" Source = "https://github.com/singingwolfboy/flask-dance" Changelog = "https://github.com/singingwolfboy/flask-dance/blob/main/CHANGELOG.rst" [project.optional-dependencies] test = [ "pytest", "pytest-mock", "responses", "freezegun", "coverage", # testing sqlalchemy support "sqlalchemy>=1.3.11", "flask-sqlalchemy", # testing integration with other extensions "flask-login", "flask-caching", "betamax", # we need the `signedtoken` extra for `oauthlib` "oauthlib[signedtoken]", ] docs = [ "sphinx>=1.3", "sphinxcontrib-seqdiag", "sphinxcontrib-spelling", "Flask-Sphinx-Themes", # code dependencies, needed for imports "sqlalchemy>=1.3.11", "pytest", "betamax", "pillow<=9.5" ] sqla = ["sqlalchemy>=1.3.11"] signals = ["blinker"] [project.entry-points.pytest11] pytest_flask_dance = "flask_dance.fixtures.pytest" [tool.flit.module] name = "flask_dance" [tool.distutils.bdist_wheel] universal = true [tool.isort] profile = "black" [tool.pytest.ini_options] markers = ["install_required: can only pass if flask_dance is installed"] [tool.coverage.run] source = ["flask_dance"] flask-dance-7.1.0/tests/000077500000000000000000000000001457161140100150315ustar00rootroot00000000000000flask-dance-7.1.0/tests/conftest.py000066400000000000000000000005201457161140100172250ustar00rootroot00000000000000import pytest import responses as resp_module @pytest.fixture def responses(request): """ Set up the `responses` module for mocking HTTP requests https://github.com/getsentry/responses """ resp_module.start() def done(): resp_module.stop() resp_module.reset() request.addfinalizer(done) flask-dance-7.1.0/tests/consumer/000077500000000000000000000000001457161140100166645ustar00rootroot00000000000000flask-dance-7.1.0/tests/consumer/storage/000077500000000000000000000000001457161140100203305ustar00rootroot00000000000000flask-dance-7.1.0/tests/consumer/storage/test_sqla.py000066400000000000000000000601551457161140100227100ustar00rootroot00000000000000import pytest sa = pytest.importorskip("sqlalchemy") import os import flask import responses from flask_caching import Cache from flask_login import ( FlaskLoginClient, LoginManager, UserMixin, current_user, login_user, logout_user, ) from flask_sqlalchemy import SQLAlchemy from sqlalchemy import event from flask_dance.consumer import OAuth2ConsumerBlueprint, oauth_authorized, oauth_error from flask_dance.consumer.storage.sqla import OAuthConsumerMixin, SQLAlchemyStorage try: import blinker except ImportError: blinker = None requires_blinker = pytest.mark.skipif(not blinker, reason="requires blinker") pytestmark = [pytest.mark.usefixtures("responses")] @pytest.fixture def blueprint(): "Make a OAuth2 blueprint for a fictional OAuth provider" bp = OAuth2ConsumerBlueprint( "test-service", __name__, client_id="client_id", client_secret="client_secret", state="random-string", base_url="https://example.com", authorization_url="https://example.com/oauth/authorize", token_url="https://example.com/oauth/access_token", redirect_url="/oauth_done", ) responses.add( responses.POST, "https://example.com/oauth/access_token", body='{"access_token":"foobar","token_type":"bearer","scope":""}', ) return bp @pytest.fixture def db(): "Make a Flask-SQLAlchemy instance" return SQLAlchemy() @pytest.fixture def app(blueprint, db, request): "Make a Flask app, attach Flask-SQLAlchemy, and establish an app context" app = flask.Flask(__name__) app.config["SERVER_NAME"] = "a.b.c" app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URI", "sqlite://") app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["CACHE_TYPE"] = "SimpleCache" app.secret_key = "secret" app.register_blueprint(blueprint, url_prefix="/login") db.init_app(app) # establish app context ctx = app.app_context() ctx.push() request.addfinalizer(ctx.pop) return app class record_queries: """ A context manager for recording the SQLAlchemy queries that were executed in a given context block. """ def __init__(self, target, identifier="before_cursor_execute"): self.target = target self.identifier = identifier def record_query(self, conn, cursor, statement, parameters, context, executemany): self.queries.append(statement) def __enter__(self): self.queries = [] event.listen(self.target, self.identifier, self.record_query) return self.queries def __exit__(self, exc_type, exc_value, traceback): event.remove(self.target, self.identifier, self.record_query) def test_sqla_storage_without_user(app, db, blueprint, request): class OAuth(OAuthConsumerMixin, db.Model): pass blueprint.storage = SQLAlchemyStorage(OAuth, db.session) db.create_all() def done(): db.session.remove() db.drop_all() request.addfinalizer(done) with record_queries(db.engine) as queries: with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ( "https://a.b.c/oauth_done", "/oauth_done", ) assert len(queries) == 2 # check the database authorizations = OAuth.query.all() assert len(authorizations) == 1 oauth = authorizations[0] assert oauth.provider == "test-service" assert isinstance(oauth.token, dict) assert oauth.token == { "access_token": "foobar", "token_type": "bearer", "scope": [""], } def test_sqla_model_repr(app, db, request): class MyAwesomeOAuth(OAuthConsumerMixin, db.Model): pass db.create_all() def done(): db.session.remove() db.drop_all() request.addfinalizer(done) o = MyAwesomeOAuth() assert "MyAwesomeOAuth" in repr(o) o.provider = "supercool" assert 'provider="supercool"' in repr(o) o.token = {"access_token": "secret"} assert "secret" not in repr(o) db.session.add(o) db.session.commit() assert "id=" in repr(o) assert "secret" not in repr(o) def test_sqla_storage(app, db, blueprint, request): class User(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80)) class OAuth(OAuthConsumerMixin, db.Model): user_id = db.Column(db.Integer, db.ForeignKey(User.id)) user = db.relationship(User) db.create_all() def done(): db.session.remove() db.drop_all() request.addfinalizer(done) # for now, we'll assume that Alice is the only user alice = User(name="Alice") db.session.add(alice) db.session.commit() # load alice's ID -- this issues a database query alice.id blueprint.storage = SQLAlchemyStorage(OAuth, db.session, user=alice) with record_queries(db.engine) as queries: with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ( "https://a.b.c/oauth_done", "/oauth_done", ) assert len(queries) == 3 # check the database alice = User.query.first() authorizations = OAuth.query.all() assert len(authorizations) == 1 oauth = authorizations[0] assert oauth.user_id == alice.id assert oauth.provider == "test-service" assert isinstance(oauth.token, dict) assert oauth.token == { "access_token": "foobar", "token_type": "bearer", "scope": [""], } def test_sqla_load_token_for_user(app, db, blueprint, request): class User(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80)) class OAuth(OAuthConsumerMixin, db.Model): user_id = db.Column(db.Integer, db.ForeignKey(User.id)) user = db.relationship(User) db.create_all() def done(): db.session.remove() db.drop_all() request.addfinalizer(done) # set token storage blueprint.storage = SQLAlchemyStorage(OAuth, db.session) # make users and OAuth tokens for several people alice = User(name="Alice") alice_token = {"access_token": "alice123", "token_type": "bearer"} alice_oauth = OAuth(user=alice, token=alice_token, provider="test-service") bob = User(name="Bob") bob_token = {"access_token": "bob456", "token_type": "bearer"} bob_oauth = OAuth(user=bob, token=bob_token, provider="test-service") sue = User(name="Sue") sue_token = {"access_token": "sue789", "token_type": "bearer"} sue_oauth = OAuth(user=sue, token=sue_token, provider="test-service") db.session.add_all([alice, bob, sue, alice_oauth, bob_oauth, sue_oauth]) db.session.commit() # by default, we should not have a token for anyone sess = blueprint.session assert not sess.token assert not blueprint.token # load token for various users blueprint.config["user"] = alice assert sess.token == alice_token assert blueprint.token == alice_token blueprint.config["user"] = bob assert sess.token == bob_token assert blueprint.token == bob_token blueprint.config["user"] = alice assert sess.token == alice_token assert blueprint.token == alice_token blueprint.config["user"] = sue assert sess.token == sue_token assert blueprint.token == sue_token # load for user ID as well del blueprint.config["user"] blueprint.config["user_id"] = bob.id assert sess.token == bob_token assert blueprint.token == bob_token # try deleting user tokens del blueprint.token assert sess.token == None assert blueprint.token == None # shouldn't affect alice's token blueprint.config["user_id"] = alice.id assert sess.token == alice_token assert blueprint.token == alice_token def test_sqla_flask_login(app, db, blueprint, request): login_manager = LoginManager(app) class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80)) class OAuth(OAuthConsumerMixin, db.Model): user_id = db.Column(db.Integer, db.ForeignKey(User.id)) user = db.relationship(User) blueprint.storage = SQLAlchemyStorage(OAuth, db.session, user=current_user) app.test_client_class = FlaskLoginClient db.create_all() def done(): db.session.remove() db.drop_all() request.addfinalizer(done) # create some users u1 = User(name="Alice") u2 = User(name="Bob") u3 = User(name="Chuck") db.session.add_all([u1, u2, u3]) db.session.commit() # configure login manager @login_manager.user_loader def load_user(userid): return User.query.get(userid) with record_queries(db.engine) as queries: with app.test_client(user=u1) as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ( "https://a.b.c/oauth_done", "/oauth_done", ) assert len(queries) == 5 # lets do it again, with Bob as the logged in user -- he gets a different token if "_login_user" in flask.g: del flask.g._login_user # clear cache responses.reset() responses.add( responses.POST, "https://example.com/oauth/access_token", body='{"access_token":"abcdef","token_type":"bearer","scope":"bob"}', ) with record_queries(db.engine) as queries: with app.test_client(user=u2) as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ( "https://a.b.c/oauth_done", "/oauth_done", ) assert len(queries) == 5 # check the database authorizations = OAuth.query.all() assert len(authorizations) == 2 u1_oauth = OAuth.query.filter_by(user=u1).one() assert u1_oauth.provider == "test-service" assert u1_oauth.token == { "access_token": "foobar", "token_type": "bearer", "scope": [""], } u2_oauth = OAuth.query.filter_by(user=u2).one() assert u2_oauth.provider == "test-service" assert u2_oauth.token == { "access_token": "abcdef", "token_type": "bearer", "scope": ["bob"], } u3_oauth = OAuth.query.filter_by(user=u3).all() assert len(u3_oauth) == 0 @requires_blinker def test_sqla_flask_login_misconfigured(app, db, blueprint, request): login_manager = LoginManager(app) class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80)) class OAuth(OAuthConsumerMixin, db.Model): user_id = db.Column(db.Integer, db.ForeignKey(User.id)) user = db.relationship(User) blueprint.storage = SQLAlchemyStorage(OAuth, db.session, user=current_user) db.create_all() def done(): db.session.remove() db.drop_all() request.addfinalizer(done) # configure login manager @login_manager.user_loader def load_user(userid): return User.query.get(userid) calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) oauth_error.connect(callback) request.addfinalizer(lambda: oauth_error.disconnect(callback)) with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ("https://a.b.c/oauth_done", "/oauth_done") assert len(calls) == 1 assert calls[0][0] == (blueprint,) error = calls[0][1]["error"] assert isinstance(error, ValueError) assert str(error) == "Cannot set OAuth token without an associated user" @requires_blinker def test_sqla_flask_login_anon_to_authed(app, db, blueprint, request): login_manager = LoginManager(app) class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80)) class OAuth(OAuthConsumerMixin, db.Model): user_id = db.Column(db.Integer, db.ForeignKey(User.id)) user = db.relationship(User) blueprint.storage = SQLAlchemyStorage(OAuth, db.session, user=current_user) db.create_all() def done(): db.session.remove() db.drop_all() request.addfinalizer(done) # configure login manager @login_manager.user_loader def load_user(userid): return User.query.get(userid) # create a user object when OAuth succeeds def logged_in(sender, token): assert token assert blueprint == sender resp = sender.session.get("/user") user = User(name=resp.json()["name"]) login_user(user) db.session.add(user) db.session.commit() flask.flash("Signed in successfully") oauth_authorized.connect(logged_in, blueprint) request.addfinalizer(lambda: oauth_authorized.disconnect(logged_in, blueprint)) # mock out the `/user` API call responses.add( responses.GET, "https://example.com/user", body='{"name":"josephine"}' ) with record_queries(db.engine) as queries: with app.test_client() as client: with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ( "https://a.b.c/oauth_done", "/oauth_done", ) assert len(queries) == 5 # check the database users = User.query.all() assert len(users) == 1 user = users[0] assert user.name == "josephine" authorizations = OAuth.query.all() assert len(authorizations) == 1 oauth = authorizations[0] assert oauth.provider == "test-service" assert oauth.token == { "access_token": "foobar", "token_type": "bearer", "scope": [""], } assert oauth.user_id == user.id def test_sqla_flask_login_preload_logged_in_user(app, db, blueprint, request): # need a URL to hit, so that tokens will be loaded, but result is irrelevant responses.add(responses.GET, "https://example.com/noop") login_manager = LoginManager(app) class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80)) class OAuth(OAuthConsumerMixin, db.Model): user_id = db.Column(db.Integer, db.ForeignKey(User.id)) user = db.relationship(User) blueprint.storage = SQLAlchemyStorage(OAuth, db.session, user=current_user) db.create_all() def done(): db.session.remove() db.drop_all() request.addfinalizer(done) # create some users, and tokens for some of them alice = User(name="Alice") alice_token = {"access_token": "alice123", "token_type": "bearer"} alice_oauth = OAuth(user=alice, token=alice_token, provider="test-service") bob = User(name="Bob") bob_token = {"access_token": "bob456", "token_type": "bearer"} bob_oauth = OAuth(user=bob, token=bob_token, provider="test-service") chuck = User(name="Chuck") # chuck doesn't get a token db.session.add_all([alice, alice_oauth, bob, bob_oauth, chuck]) db.session.commit() # configure login manager @login_manager.user_loader def load_user(userid): return User.query.get(userid) # create a simple view @app.route("/") def index(): return "success" with app.test_request_context("/"): login_user(alice) # hit /noop to load tokens blueprint.session.get("/noop") # now the flask-dance session should have Alice's token loaded assert blueprint.session.token == alice_token with app.test_request_context("/"): # set bob as the logged in user login_user(bob) # hit /noop to load tokens blueprint.session.get("/noop") # now the flask-dance session should have Bob's token loaded assert blueprint.session.token == bob_token with app.test_request_context("/"): # now let's try chuck login_user(chuck) blueprint.session.get("/noop") assert blueprint.session.token == None with app.test_request_context("/"): # no one is logged in -- this is an anonymous user logout_user() with pytest.raises(ValueError): blueprint.session.get("/noop") def test_sqla_flask_login_no_user_required(app, db, blueprint, request): # need a URL to hit, so that tokens will be loaded, but result is irrelevant responses.add(responses.GET, "https://example.com/noop") login_manager = LoginManager(app) class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80)) class OAuth(OAuthConsumerMixin, db.Model): user_id = db.Column(db.Integer, db.ForeignKey(User.id)) user = db.relationship(User) blueprint.storage = SQLAlchemyStorage( OAuth, db.session, user=current_user, user_required=False ) db.create_all() def done(): db.session.remove() db.drop_all() request.addfinalizer(done) # configure login manager @login_manager.user_loader def load_user(userid): return User.query.get(userid) # create a simple view @app.route("/") def index(): return "success" with app.test_request_context("/"): # no one is logged in -- this is an anonymous user logout_user() # this should *not* raise an error blueprint.session.get("/noop") assert blueprint.session.token == None def test_sqla_delete_token(app, db, blueprint, request): class OAuth(OAuthConsumerMixin, db.Model): pass blueprint.storage = SQLAlchemyStorage(OAuth, db.session) db.create_all() def done(): db.session.remove() db.drop_all() request.addfinalizer(done) # Create an existing OAuth token for the service existing = OAuth( provider="test-service", token={"access_token": "something", "token_type": "bearer", "scope": ["blah"]}, ) db.session.add(existing) db.session.commit() assert len(OAuth.query.all()) == 1 assert blueprint.token == { "access_token": "something", "token_type": "bearer", "scope": ["blah"], } del blueprint.token assert blueprint.token == None assert len(OAuth.query.all()) == 0 def test_sqla_overwrite_token(app, db, blueprint, request): class OAuth(OAuthConsumerMixin, db.Model): pass blueprint.storage = SQLAlchemyStorage(OAuth, db.session) db.create_all() def done(): db.session.remove() db.drop_all() request.addfinalizer(done) # Create an existing OAuth token for the service existing = OAuth( provider="test-service", token={"access_token": "something", "token_type": "bearer", "scope": ["blah"]}, ) db.session.add(existing) db.session.commit() assert len(OAuth.query.all()) == 1 with record_queries(db.engine) as queries: with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ( "https://a.b.c/oauth_done", "/oauth_done", ) assert len(queries) == 2 # check that the database record was overwritten authorizations = OAuth.query.all() assert len(authorizations) == 1 oauth = authorizations[0] assert oauth.provider == "test-service" assert isinstance(oauth.token, dict) assert oauth.token == { "access_token": "foobar", "token_type": "bearer", "scope": [""], } def test_sqla_cache(app, db, blueprint, request): cache = Cache(app) class OAuth(OAuthConsumerMixin, db.Model): pass blueprint.storage = SQLAlchemyStorage(OAuth, db.session, cache=cache) db.create_all() def done(): db.session.remove() db.drop_all() request.addfinalizer(done) with record_queries(db.engine) as queries: with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ( "https://a.b.c/oauth_done", "/oauth_done", ) assert len(queries) == 2 expected_token = {"access_token": "foobar", "token_type": "bearer", "scope": [""]} # check the database authorizations = OAuth.query.all() assert len(authorizations) == 1 oauth = authorizations[0] assert oauth.provider == "test-service" assert isinstance(oauth.token, dict) assert oauth.token == expected_token # cache should be invalidated assert cache.get("flask_dance_token|test-service|None") is None # first reference to the token should generate SQL queries with record_queries(db.engine) as queries: assert blueprint.token == expected_token assert len(queries) == 1 # should now be in the cache assert cache.get("flask_dance_token|test-service|None") == expected_token # subsequent references should not generate SQL queries with record_queries(db.engine) as queries: assert blueprint.token == expected_token assert len(queries) == 0 flask-dance-7.1.0/tests/consumer/test_oauth1.py000066400000000000000000000443131457161140100215030ustar00rootroot00000000000000from unittest import mock from urllib.parse import quote_plus import flask import pytest import responses from oauthlib.oauth1.rfc5849.utils import parse_authorization_header from werkzeug.middleware.proxy_fix import ProxyFix from flask_dance.consumer import ( OAuth1ConsumerBlueprint, oauth_authorized, oauth_before_login, oauth_error, ) from flask_dance.consumer.requests import OAuth1Session from flask_dance.consumer.storage import MemoryStorage try: import blinker except ImportError: blinker = None requires_blinker = pytest.mark.skipif(not blinker, reason="requires blinker") def make_app(login_url=None): blueprint = OAuth1ConsumerBlueprint( "test-service", __name__, client_key="client_key", client_secret="client_secret", base_url="https://example.com", request_token_url="https://example.com/oauth/request_token", access_token_url="https://example.com/oauth/access_token", authorization_url="https://example.com/oauth/authorize", redirect_to="index", login_url=login_url, ) app = flask.Flask(__name__) app.secret_key = "secret" app.register_blueprint(blueprint, url_prefix="/login") app.config["SERVER_NAME"] = "a.b.c" @app.route("/") def index(): return "index" return app, blueprint def test_generate_login_url(): app, _ = make_app() with app.test_request_context("/"): login_url = flask.url_for("test-service.login") assert login_url == "/login/test-service" def test_override_login_url(): app, _ = make_app(login_url="/crazy/custom/url") with app.test_request_context("/"): login_url = flask.url_for("test-service.login") assert login_url == "/login/crazy/custom/url" @responses.activate def test_login_url(): responses.add( responses.POST, "https://example.com/oauth/request_token", body="oauth_token=foobar&oauth_token_secret=bazqux", ) app, _ = make_app() client = app.test_client() resp = client.get( "/login/test-service", base_url="https://a.b.c", follow_redirects=False ) # check that we obtained a request token assert len(responses.calls) == 1 assert "Authorization" in responses.calls[0].request.headers auth_header = dict( parse_authorization_header( responses.calls[0].request.headers["Authorization"].decode("utf-8") ) ) assert auth_header["oauth_consumer_key"] == "client_key" assert "oauth_signature" in auth_header assert auth_header["oauth_callback"] == quote_plus( "https://a.b.c/login/test-service/authorized" ) # check that we redirected the client assert resp.status_code == 302 assert ( resp.headers["Location"] == "https://example.com/oauth/authorize?oauth_token=foobar" ) @responses.activate def test_login_url_forwarded_proto(): responses.add( responses.POST, "https://example.com/oauth/request_token", body="oauth_token=foobar&oauth_token_secret=bazqux", ) app, _ = make_app() app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1) with app.test_client() as client: resp = client.get( "/login/test-service", base_url="http://a.b.c", headers={"X-Forwarded-Proto": "https"}, follow_redirects=False, ) auth_header = dict( parse_authorization_header( responses.calls[0].request.headers["Authorization"].decode("utf-8") ) ) # this should be https assert auth_header["oauth_callback"] == quote_plus( "https://a.b.c/login/test-service/authorized" ) @responses.activate def test_authorized_url(): responses.add( responses.POST, "https://example.com/oauth/access_token", body="oauth_token=xxx&oauth_token_secret=yyy", ) app, _ = make_app() with app.test_client() as client: resp = client.get( "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ("https://a.b.c/", "/") # check that we obtained an access token assert len(responses.calls) == 1 assert "Authorization" in responses.calls[0].request.headers auth_header = dict( parse_authorization_header( responses.calls[0].request.headers["Authorization"].decode("utf-8") ) ) assert auth_header["oauth_consumer_key"] == "client_key" assert auth_header["oauth_token"] == "foobar" assert auth_header["oauth_verifier"] == "xyz" # check that we stored the access token and secret in the session assert flask.session["test-service_oauth_token"] == { "oauth_token": "xxx", "oauth_token_secret": "yyy", } @responses.activate def test_redirect_url(): responses.add( responses.POST, "https://example.com/oauth/access_token", body="oauth_token=xxx&oauth_token_secret=yyy", ) blueprint = OAuth1ConsumerBlueprint( "test-service", __name__, client_key="client_key", client_secret="client_secret", base_url="https://example.com", request_token_url="https://example.com/oauth/request_token", access_token_url="https://example.com/oauth/access_token", authorization_url="https://example.com/oauth/authorize", redirect_url="http://mysite.cool/whoa?query=basketball", ) app = flask.Flask(__name__) app.secret_key = "secret" app.register_blueprint(blueprint, url_prefix="/login") with app.test_client() as client: resp = client.get( "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] == "http://mysite.cool/whoa?query=basketball" @responses.activate def test_redirect_to(): responses.add( responses.POST, "https://example.com/oauth/access_token", body="oauth_token=xxx&oauth_token_secret=yyy", ) blueprint = OAuth1ConsumerBlueprint( "test-service", __name__, client_key="client_key", client_secret="client_secret", base_url="https://example.com", request_token_url="https://example.com/oauth/request_token", access_token_url="https://example.com/oauth/access_token", authorization_url="https://example.com/oauth/authorize", redirect_to="my_view", ) app = flask.Flask(__name__) app.secret_key = "secret" app.register_blueprint(blueprint, url_prefix="/login") @app.route("/blargl") def my_view(): return "check out my url" with app.test_client() as client: resp = client.get( "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ("https://a.b.c/blargl", "/blargl") @responses.activate def test_redirect_fallback(): responses.add( responses.POST, "https://example.com/oauth/access_token", body="oauth_token=xxx&oauth_token_secret=yyy", ) blueprint = OAuth1ConsumerBlueprint( "test-service", __name__, client_key="client_key", client_secret="client_secret", base_url="https://example.com", request_token_url="https://example.com/oauth/request_token", access_token_url="https://example.com/oauth/access_token", authorization_url="https://example.com/oauth/authorize", ) app = flask.Flask(__name__) app.secret_key = "secret" app.register_blueprint(blueprint, url_prefix="/login") with app.test_client() as client: resp = client.get( "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ("https://a.b.c/", "/") def test_authorization_required_decorator_allowed(): app, blueprint = make_app() @app.route("/restricted") @blueprint.session.authorization_required def restricted_view(): return "allowed" blueprint.storage = MemoryStorage( {"oauth_token": "test1", "oauth_token_secret": "test2"} ) with app.test_client() as client: resp = client.get("/restricted", base_url="https://a.b.c") assert resp.status_code == 200 text = resp.get_data(as_text=True) assert text == "allowed" def test_authorization_required_decorator_redirect(): app, blueprint = make_app() @app.route("/restricted") @blueprint.session.authorization_required def restricted_view(): return "allowed" with app.test_client() as client: resp = client.get("/restricted", base_url="https://a.b.c") # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ( "https://a.b.c/login/test-service", "/login/test-service", ) @requires_blinker def test_signal_oauth_authorized(request): app, bp = make_app() fake_token = {"access_token": "test-token"} bp.session.fetch_access_token = mock.Mock(return_value=fake_token) calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) oauth_authorized.connect(callback) request.addfinalizer(lambda: oauth_authorized.disconnect(callback)) with app.test_client() as client: resp = client.get( "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz" ) # check that we stored the token assert flask.session["test-service_oauth_token"] == fake_token assert len(calls), 1 assert calls[0][0] == (bp,) assert calls[0][1] == {"token": fake_token} @requires_blinker def test_signal_oauth_authorized_abort(request): app, bp = make_app() bp.session.fetch_access_token = mock.Mock(return_value="test-token") calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) return False oauth_authorized.connect(callback) request.addfinalizer(lambda: oauth_authorized.disconnect(callback)) with app.test_client() as client: resp = client.get( "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz" ) # check that we did NOT store the token assert "test-token_oauth_token" not in flask.session # the callback should still have been called assert len(calls) == 1 @requires_blinker def test_signal_oauth_before_login(request): app, bp = make_app() bp.session.fetch_request_token = mock.Mock(return_value="test-token") def callback(*args, **kwargs): del flask.session["user_id"] return False oauth_before_login.connect(callback) request.addfinalizer(lambda: oauth_before_login.disconnect(callback)) with app.test_request_context(): with app.test_client() as client: flask.session["user_id"] = 1 assert flask.session["user_id"] == 1 client.get("/login/test-service") assert "user_id" not in flask.session @requires_blinker def test_signal_oauth_authorized_response(request): app, bp = make_app() bp.session.fetch_access_token = mock.Mock(return_value="test-token") calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) return flask.redirect("/url") oauth_authorized.connect(callback) request.addfinalizer(lambda: oauth_authorized.disconnect(callback)) with app.test_client() as client: resp = client.get( "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz" ) assert resp.status_code == 302 assert resp.headers["Location"] in ("/url", "http://localhost/url") # check that we did NOT store the token assert "test-token_oauth_token" not in flask.session # the callback should still have been called assert len(calls) == 1 @requires_blinker def test_signal_sender_oauth_authorized(request): app, bp = make_app() bp2 = OAuth1ConsumerBlueprint( "test2", __name__, client_key="client_key", client_secret="client_secret", base_url="https://example.com", request_token_url="https://example.com/oauth/request_token", access_token_url="https://example.com/oauth/access_token", authorization_url="https://example.com/oauth/authorize", redirect_to="index", ) app.register_blueprint(bp2, url_prefix="/login") calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) oauth_authorized.connect(callback, sender=bp) request.addfinalizer(lambda: oauth_authorized.disconnect(callback, sender=bp)) with app.test_client() as client: bp.session.fetch_access_token = mock.Mock(return_value="test-token") bp2.session.fetch_access_token = mock.Mock(return_value="test2-token") resp = client.get( "/login/test2/authorized?oauth_token=foobar&oauth_verifier=xyz" ) assert len(calls) == 0 with app.test_client() as client: bp.session.fetch_access_token = mock.Mock(return_value="test-token") bp2.session.fetch_access_token = mock.Mock(return_value="test2-token") resp = client.get( "/login/test-service/authorized?oauth_token=foobar&oauth_verifier=xyz" ) assert len(calls) == 1 assert calls[0][0] == (bp,) assert calls[0][1] == {"token": "test-token"} with app.test_client() as client: bp.session.fetch_access_token = mock.Mock(return_value="test-token") bp2.session.fetch_access_token = mock.Mock(return_value="test2-token") resp = client.get( "/login/test2/authorized?oauth_token=foobar&oauth_verifier=xyz" ) assert len(calls) == 1 # unchanged @requires_blinker @responses.activate def test_signal_oauth_error_login(request): responses.add( responses.POST, "https://example.com/oauth/request_token", body="oauth_problem=nonce_used", status=401, ) app, bp = make_app() calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) oauth_error.connect(callback) request.addfinalizer(lambda: oauth_error.disconnect(callback)) with app.test_client() as client: resp = client.get("/login/test-service", base_url="https://a.b.c") assert len(calls) == 1 assert calls[0][0] == (bp,) assert ( calls[0][1]["message"] == "Token request failed with code 401, response was 'oauth_problem=nonce_used'." ) assert resp.status_code == 302 location = resp.headers["Location"] assert location in ("/", "https://a.b.c/") @requires_blinker @responses.activate def test_signal_oauth_error_authorized(request): responses.add( responses.POST, "https://example.com/oauth/access_token", body="Invalid request token.", status=401, ) app, bp = make_app() calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) oauth_error.connect(callback) request.addfinalizer(lambda: oauth_error.disconnect(callback)) with app.test_client() as client: resp = client.get( "/login/test-service/authorized?" "oauth_token=faketoken&" "oauth_token_secret=fakesecret&" "oauth_verifier=fakeverifier", base_url="https://a.b.c", ) assert len(calls) == 1 assert calls[0][0] == (bp,) assert ( calls[0][1]["message"] == "Token request failed with code 401, response was 'Invalid request token.'." ) assert resp.status_code == 302 @requires_blinker def test_signal_oauth_notoken_authorized(request): app, bp = make_app() calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) oauth_error.connect(callback) request.addfinalizer(lambda: oauth_error.disconnect(callback)) with app.test_client() as client: resp = client.get( "/login/test-service/authorized?" "denied=faketoken", base_url="https://a.b.c", ) assert len(calls) == 1 assert calls[0][0] == (bp,) assert "Response does not contain a token" in calls[0][1]["message"] assert calls[0][1]["response"] == {"denied": "faketoken"} assert resp.status_code == 302 location = resp.headers["Location"] assert location in ("/", "https://a.b.c/") class CustomOAuth1Session(OAuth1Session): my_attr = "foobar" def test_custom_session_class(): bp = OAuth1ConsumerBlueprint( "test", __name__, client_key="client_key", client_secret="client_secret", base_url="https://example.com", request_token_url="https://example.com/oauth/request_token", access_token_url="https://example.com/oauth/access_token", authorization_url="https://example.com/oauth/authorize", redirect_to="index", session_class=CustomOAuth1Session, ) assert isinstance(bp.session, CustomOAuth1Session) assert bp.session.my_attr == "foobar" def test_rule_kwargs(): blueprint = OAuth1ConsumerBlueprint( "test-service", __name__, client_key="client_key", client_secret="client_secret", base_url="https://example.com", request_token_url="https://example.com/oauth/request_token", access_token_url="https://example.com/oauth/access_token", authorization_url="https://example.com/oauth/authorize", redirect_to="my_view", rule_kwargs={"host": "example2.com"}, ) app = flask.Flask(__name__) app.secret_key = "secret" app.register_blueprint(blueprint, url_prefix="/login") rules = [ rule for rule in app.url_map.iter_rules() if rule.endpoint.startswith("test-service.") ] assert all(rule.host == "example2.com" for rule in rules) assert len(rules) == 2 flask-dance-7.1.0/tests/consumer/test_oauth2.py000066400000000000000000000703741457161140100215120ustar00rootroot00000000000000import json import re from unittest import mock from urllib.parse import parse_qsl import flask import pytest import responses from freezegun import freeze_time from oauthlib.oauth2 import MissingCodeError from oauthlib.oauth2.rfc6749.clients import Client as OAuth2Client from urlobject import URLObject from werkzeug.middleware.proxy_fix import ProxyFix from flask_dance.consumer import ( OAuth2ConsumerBlueprint, oauth_authorized, oauth_before_login, oauth_error, ) from flask_dance.consumer.requests import OAuth2Session from flask_dance.consumer.storage import MemoryStorage try: import blinker except ImportError: blinker = None requires_blinker = pytest.mark.skipif(not blinker, reason="requires blinker") def make_app(login_url=None, debug=False, **kwargs): blueprint = OAuth2ConsumerBlueprint( "test-service", __name__, client_id="client_id", client_secret="client_secret", scope="admin", state="random-string", base_url="https://example.com", authorization_url="https://example.com/oauth/authorize", token_url="https://example.com/oauth/access_token", redirect_to="index", login_url=login_url, **kwargs, ) app = flask.Flask(__name__) app.secret_key = "secret" app.register_blueprint(blueprint, url_prefix="/login") app.debug = debug app.config["SERVER_NAME"] = "a.b.c" @app.route("/") def index(): return "index" return app, blueprint def test_generate_login_url(): app, _ = make_app() with app.test_request_context("/"): login_url = flask.url_for("test-service.login") assert login_url == "/login/test-service" def test_override_login_url(): app, _ = make_app(login_url="/crazy/custom/url") with app.test_request_context("/"): login_url = flask.url_for("test-service.login") assert login_url == "/login/crazy/custom/url" @responses.activate def test_login_url(): app, _ = make_app() with app.test_client() as client: resp = client.get( "/login/test-service", base_url="https://a.b.c", follow_redirects=False ) # check that we saved the state in the session assert flask.session["test-service_oauth_state"] == "random-string" # check that we redirected the client assert resp.status_code == 302 location = URLObject(resp.headers["Location"]) assert location.without_query() == "https://example.com/oauth/authorize" assert location.query_dict["client_id"] == "client_id" assert ( location.query_dict["redirect_uri"] == "https://a.b.c/login/test-service/authorized" ) assert location.query_dict["scope"] == "admin" assert location.query_dict["state"] == "random-string" @responses.activate def test_login_url_with_pkce(): code_challenge_method = "S256" # That should be a default value app, _ = make_app(use_pkce=True) with app.test_client() as client: resp = client.get( "/login/test-service", base_url="https://a.b.c", follow_redirects=False ) # check that we saved the code verifier in the session assert "test-service_oauth_code_verifier" in flask.session code_verifier = flask.session["test-service_oauth_code_verifier"] assert 43 <= len(code_verifier) <= 128 # RFC7636 section 4.1 code_challenge = OAuth2Client("123").create_code_challenge( code_verifier, code_challenge_method ) # check that we redirected the client assert resp.status_code == 302 location = URLObject(resp.headers["Location"]) assert location.without_query() == "https://example.com/oauth/authorize" # check PKCE specific query parameters assert location.query_dict["code_challenge_method"] == code_challenge_method assert location.query_dict["code_challenge"] == code_challenge @responses.activate def test_login_url_with_invalid_code_challenge_method(): app, _ = make_app(use_pkce=True, code_challenge_method="MD5") with app.test_client() as client: resp = client.get( "/login/test-service", base_url="https://a.b.c", follow_redirects=False ) # the code verifier is saved in the session ... assert "test-service_oauth_code_verifier" in flask.session location = URLObject(resp.headers["Location"]) assert location.without_query() == "https://example.com/oauth/authorize" # ... but because the "code challenge method" was invalid it was not added to the query parameters assert "code_challenge_method" not in location.query_dict assert "code_challenge" not in location.query_dict @responses.activate def test_login_url_forwarded_proto(): app, _ = make_app() app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1) with app.test_client() as client: resp = client.get( "/login/test-service", base_url="http://a.b.c", headers={"X-Forwarded-Proto": "https"}, follow_redirects=False, ) # check that we redirected the client with a https redirect_uri assert resp.status_code == 302 location = URLObject(resp.headers["Location"]) assert ( location.query_dict["redirect_uri"] == "https://a.b.c/login/test-service/authorized" ) @responses.activate def test_authorized_url(): responses.add( responses.POST, "https://example.com/oauth/access_token", body='{"access_token":"foobar","token_type":"bearer","scope":"admin"}', ) app, _ = make_app() with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ("https://a.b.c/", "/") # check that we obtained an access token assert len(responses.calls) == 1 request_data = dict(parse_qsl(responses.calls[0].request.body)) assert ( request_data["redirect_uri"] == "https://a.b.c/login/test-service/authorized" ) # check that we stored the access token in the session assert flask.session["test-service_oauth_token"] == { "access_token": "foobar", "scope": ["admin"], "token_type": "bearer", } def test_authorized_url_no_state(): app, _ = make_app() with app.test_client() as client: # make the request, without resetting the session beforehand resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client back to login view assert resp.status_code == 302 assert resp.headers["Location"] in ( "https://a.b.c/login/test-service", "/login/test-service", ) # check that there's nothing in the session assert "test-service_oauth_token" not in flask.session @responses.activate def test_authorized_url_behind_proxy(): responses.add( responses.POST, "https://example.com/oauth/access_token", body='{"access_token":"foobar","token_type":"bearer","scope":"admin"}', ) app, _ = make_app() app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1) with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="http://a.b.c", headers={"X-Forwarded-Proto": "https"}, ) request_data = dict(parse_qsl(responses.calls[0].request.body)) # this should be https assert ( request_data["redirect_uri"] == "https://a.b.c/login/test-service/authorized" ) def test_authorized_url_invalid_response(): app, _ = make_app(debug=True) with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request with pytest.raises(MissingCodeError) as missingError: client.get( "/login/test-service/authorized?state=random-string&error_code=1349048&error_message=IMUSEFUL", base_url="https://a.b.c", ) match = re.search(r"{[^}]*}", str(missingError.value)) err_dict = json.loads(match.group(0)) assert err_dict == { "state": "random-string", "error_message": "IMUSEFUL", "error_code": "1349048", } @responses.activate @freeze_time("2016-01-01 12:00:01") def test_authorized_url_token_lifetime(): responses.add( responses.POST, "https://example.com/oauth/access_token", body='{"access_token":"foobar","token_type":"bearer","expires_in":300}', ) app, _ = make_app() with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ("https://a.b.c/", "/") # check that we obtained an access token assert len(responses.calls) == 1 request_data = dict(parse_qsl(responses.calls[0].request.body)) assert ( request_data["redirect_uri"] == "https://a.b.c/login/test-service/authorized" ) # check that we stored the access token and expiration date in the session expected_stored_token = { "access_token": "foobar", "token_type": "bearer", "expires_in": 300, "expires_at": 1451649901, } assert flask.session["test-service_oauth_token"] == expected_stored_token @responses.activate def test_authorized_url_with_pkce(): responses.add( responses.POST, "https://example.com/oauth/access_token", body='{"access_token":"foobar","token_type":"bearer","scope":"admin"}', ) app, _ = make_app(use_pkce=True) _state = "random-string" _code_verifier = "random-code-very-secure" with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = _state sess[f"test-service_oauth_code_verifier"] = _code_verifier # make the request resp = client.get( f"/login/test-service/authorized?code=secret-code&state={_state}&code_verifier={_code_verifier}", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ("https://a.b.c/", "/") # check that we obtained an access token assert len(responses.calls) == 1 request_data = dict(parse_qsl(responses.calls[0].request.body)) assert ( request_data["redirect_uri"] == "https://a.b.c/login/test-service/authorized" ) assert request_data["code_verifier"] == _code_verifier # check that we stored the access token in the session assert flask.session["test-service_oauth_token"] == { "access_token": "foobar", "scope": ["admin"], "token_type": "bearer", } def test_authorized_url_pkce_flow_no_code_verifier(): app, _ = make_app(use_pkce=True) with app.test_client() as client: # make the request, without adding the code_verifier to the session with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client back to login view assert resp.status_code == 302 assert resp.headers["Location"] in ( "https://a.b.c/login/test-service", "/login/test-service", ) # check that there's nothing in the session assert "test-service_oauth_token" not in flask.session def test_return_expired_token(request): app, bp = make_app() time1 = "2016-01-01 12:00:01" time2 = "2016-01-01 12:05:00" # 299 sec in future time3 = "2016-01-01 12:10:01" # 600 sec in future token = { "access_token": "foobar", "token_type": "bearer", "expires_in": 300, # expires in 300 seconds } ctx = app.test_request_context("/") request.addfinalizer(ctx.pop) ctx.push() with freeze_time(time1): bp.token = token modified1 = token.copy() modified1["expires_at"] = 1451649901 assert bp.token == modified1 with freeze_time(time2): modified2 = token.copy() modified2["expires_in"] = 1 modified2["expires_at"] = 1451649901 assert bp.token == modified2 with freeze_time(time3): modified3 = token.copy() modified3["expires_in"] = -300 modified3["expires_at"] = 1451649901 assert bp.token == modified3 @responses.activate def test_provider_error(): app, _ = make_app() with app.test_client() as client: # make the request resp = client.get( "/login/test-service/authorized?" "error=invalid_redirect&" "error_description=Invalid+redirect_URI&" "error_uri=https%3a%2f%2fexample.com%2fdocs%2fhelp", base_url="https://a.b.c", ) # even though there was an error, we should still redirect the client assert resp.status_code == 302 assert resp.headers["Location"] in ("https://a.b.c/", "/") # shouldn't even try getting an access token, though assert len(responses.calls) == 0 @responses.activate def test_redirect_url(): responses.add( responses.POST, "https://example.com/oauth/access_token", body='{"access_token":"foobar","token_type":"bearer","scope":"admin"}', ) blueprint = OAuth2ConsumerBlueprint( "test-service", __name__, client_id="client_id", client_secret="client_secret", state="random-string", base_url="https://example.com", authorization_url="https://example.com/oauth/authorize", token_url="https://example.com/oauth/access_token", redirect_url="http://mysite.cool/whoa?query=basketball", ) app = flask.Flask(__name__) app.secret_key = "secret" app.config["SERVER_NAME"] = "a.b.c" app.register_blueprint(blueprint, url_prefix="/login") with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] == "http://mysite.cool/whoa?query=basketball" @responses.activate def test_redirect_to(): responses.add( responses.POST, "https://example.com/oauth/access_token", body='{"access_token":"foobar","token_type":"bearer","scope":"admin"}', ) blueprint = OAuth2ConsumerBlueprint( "test-service", __name__, client_id="client_id", client_secret="client_secret", state="random-string", base_url="https://example.com", authorization_url="https://example.com/oauth/authorize", token_url="https://example.com/oauth/access_token", redirect_to="my_view", ) app = flask.Flask(__name__) app.secret_key = "secret" app.config["SERVER_NAME"] = "a.b.c" app.register_blueprint(blueprint, url_prefix="/login") @app.route("/blargl") def my_view(): return "check out my url" with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ("https://a.b.c/blargl", "/blargl") @responses.activate def test_redirect_fallback(): responses.add( responses.POST, "https://example.com/oauth/access_token", body='{"access_token":"foobar","token_type":"bearer","scope":"admin"}', ) blueprint = OAuth2ConsumerBlueprint( "test-service", __name__, client_id="client_id", client_secret="client_secret", state="random-string", base_url="https://example.com", authorization_url="https://example.com/oauth/authorize", token_url="https://example.com/oauth/access_token", ) app = flask.Flask(__name__) app.secret_key = "secret" app.config["SERVER_NAME"] = "a.b.c" app.register_blueprint(blueprint, url_prefix="/login") @app.route("/blargl") def my_view(): return "check out my url" with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" # make the request resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ("https://a.b.c/", "/") def test_authorization_required_decorator_allowed(): app, blueprint = make_app() @app.route("/restricted") @blueprint.session.authorization_required def restricted_view(): return "allowed" blueprint.storage = MemoryStorage({"access_token": "faketoken"}) with app.test_client() as client: resp = client.get("/restricted", base_url="https://a.b.c") assert resp.status_code == 200 text = resp.get_data(as_text=True) assert text == "allowed" def test_authorization_required_decorator_redirect(): app, blueprint = make_app() @app.route("/restricted") @blueprint.session.authorization_required def restricted_view(): return "allowed" with app.test_client() as client: resp = client.get("/restricted", base_url="https://a.b.c") # check that we redirected the client assert resp.status_code == 302 assert resp.headers["Location"] in ( "https://a.b.c/login/test-service", "/login/test-service", ) @requires_blinker def test_signal_oauth_authorized(request): app, bp = make_app() calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) oauth_authorized.connect(callback) request.addfinalizer(lambda: oauth_authorized.disconnect(callback)) fake_token = {"access_token": "test-token"} with app.test_client() as client: with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" bp.session.fetch_token = mock.Mock(return_value=fake_token) resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string" ) assert resp.status_code == 302 # check that we stored the token assert flask.session["test-service_oauth_token"] == fake_token assert len(calls) == 1 assert calls[0][0] == (bp,) assert calls[0][1] == {"token": fake_token} @requires_blinker def test_signal_oauth_before_login(request): app, bp = make_app() def callback(*args, **kwargs): del flask.session["user_id"] oauth_before_login.connect(callback) request.addfinalizer(lambda: oauth_before_login.disconnect(callback)) with app.test_request_context(): with app.test_client() as client: flask.session["user_id"] = 1 assert flask.session["user_id"] == 1 client.get("/login/test-service") assert "user_id" not in flask.session @requires_blinker def test_signal_oauth_authorized_abort(request): app, bp = make_app() calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) return False oauth_authorized.connect(callback) request.addfinalizer(lambda: oauth_authorized.disconnect(callback)) fake_token = {"access_token": "test-token"} with app.test_client() as client: with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" bp.session.fetch_token = mock.Mock(return_value=fake_token) resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string" ) # check that we did NOT store the token assert "test-service_oauth_token" not in flask.session # callback still should have been called assert len(calls) == 1 @requires_blinker def test_signal_oauth_authorized_response(request): app, bp = make_app() calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) return flask.redirect("/url") oauth_authorized.connect(callback) request.addfinalizer(lambda: oauth_authorized.disconnect(callback)) fake_token = {"access_token": "test-token"} with app.test_client() as client: with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" bp.session.fetch_token = mock.Mock(return_value=fake_token) resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string" ) assert resp.status_code == 302 assert resp.headers["Location"] in ("/url", "http://localhost/url") # check that we did NOT store the token assert "test-service_oauth_token" not in flask.session # callback still should have been called assert len(calls) == 1 @requires_blinker def test_signal_sender_oauth_authorized(request): app, bp = make_app() bp2 = OAuth2ConsumerBlueprint( "test2", __name__, client_id="client_id", client_secret="client_secret", scope="admin", state="random-string", base_url="https://example.com", authorization_url="https://example.com/oauth/authorize", token_url="https://example.com/oauth/access_token", redirect_to="index", ) app.register_blueprint(bp2, url_prefix="/login") calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) oauth_authorized.connect(callback, sender=bp) request.addfinalizer(lambda: oauth_authorized.disconnect(callback, sender=bp)) fake_token = {"access_token": "test-token"} fake_token2 = {"access_token": "test-token2"} with app.test_client() as client: with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" bp.session.fetch_token = mock.Mock(return_value=fake_token) bp2.session.fetch_token = mock.Mock(return_value=fake_token2) resp = client.get( "/login/test2/authorized?code=secret-code&state=random-string" ) assert len(calls) == 0 with app.test_client() as client: with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" bp.session.fetch_token = mock.Mock(return_value="test-token") bp2.session.fetch_token = mock.Mock(return_value="test2-token") resp = client.get( "/login/test-service/authorized?code=secret-code&state=random-string" ) assert len(calls) == 1 assert calls[0][0] == (bp,) assert calls[0][1] == {"token": "test-token"} with app.test_client() as client: with client.session_transaction() as sess: sess["test-service_oauth_state"] = "random-string" bp.session.fetch_token = mock.Mock(return_value=fake_token) bp2.session.fetch_token = mock.Mock(return_value=fake_token2) resp = client.get( "/login/test2/authorized?code=secret-code&state=random-string" ) assert len(calls) == 1 # unchanged @requires_blinker def test_signal_oauth_error(request): app, bp = make_app() calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) oauth_error.connect(callback) request.addfinalizer(lambda: oauth_error.disconnect(callback)) with app.test_client() as client: resp = client.get( "/login/test-service/authorized?" "error=unauthorized_client&" "error_description=Invalid+redirect+URI&" "error_uri=https%3a%2f%2fexample.com%2fdocs%2fhelp", base_url="https://a.b.c", ) assert len(calls) == 1 assert calls[0][0] == (bp,) assert calls[0][1] == { "error": "unauthorized_client", "error_description": "Invalid redirect URI", "error_uri": "https://example.com/docs/help", } assert resp.status_code == 302 @requires_blinker def test_signal_oauth_error_response(request): app, bp = make_app() calls = [] def callback(*args, **kwargs): calls.append((args, kwargs)) return flask.redirect("/url") oauth_error.connect(callback) request.addfinalizer(lambda: oauth_error.disconnect(callback)) with app.test_client() as client: resp = client.get( "/login/test-service/authorized?" "error=unauthorized_client&" "error_description=Invalid+redirect+URI&" "error_uri=https%3a%2f%2fexample.com%2fdocs%2fhelp", base_url="https://a.b.c", ) assert resp.status_code == 302 assert resp.headers["Location"] in ("/url", "http://localhost/url") assert len(calls) == 1 assert calls[0] == ( (bp,), { "error": "unauthorized_client", "error_description": "Invalid redirect URI", "error_uri": "https://example.com/docs/help", }, ) class CustomOAuth2Session(OAuth2Session): my_attr = "foobar" def test_custom_session_class(): bp = OAuth2ConsumerBlueprint( "test", __name__, client_id="client_id", client_secret="client_secret", scope="admin", state="random-string", base_url="https://example.com", authorization_url="https://example.com/oauth/authorize", token_url="https://example.com/oauth/access_token", redirect_to="index", session_class=CustomOAuth2Session, ) assert isinstance(bp.session, CustomOAuth2Session) assert bp.session.my_attr == "foobar" def test_rule_kwargs(): blueprint = OAuth2ConsumerBlueprint( "test-service", __name__, client_id="client_id", client_secret="client_secret", state="random-string", base_url="https://example.com", authorization_url="https://example.com/oauth/authorize", token_url="https://example.com/oauth/access_token", redirect_url="http://mysite.cool/whoa?query=basketball", rule_kwargs={"host": "example2.com"}, ) app = flask.Flask(__name__) app.secret_key = "secret" app.register_blueprint(blueprint, url_prefix="/login") rules = [ rule for rule in app.url_map.iter_rules() if rule.endpoint.startswith("test-service.") ] assert all(rule.host == "example2.com" for rule in rules) assert len(rules) == 2 flask-dance-7.1.0/tests/consumer/test_requests.py000066400000000000000000000051621457161140100221540ustar00rootroot00000000000000from unittest import mock import pytest import responses from flask_dance.consumer.requests import OAuth1Session, OAuth2Session FAKE_OAUTH1_TOKEN = {"oauth_token": "abcdefg", "oauth_token_secret": "hijklmnop"} FAKE_OAUTH2_TOKEN = { "access_token": "deadbeef", "scope": ["custom"], "token_type": "bearer", } def test_oauth1session_authorized(): bp = mock.Mock(token=FAKE_OAUTH1_TOKEN) sess = OAuth1Session(client_key="ckey", client_secret="csec", blueprint=bp) sess.load_token = mock.Mock(wraps=sess.load_token) assert sess.authorized == True assert sess.load_token.called def test_oauth1session_not_authorized(): bp = mock.Mock(token=None) sess = OAuth1Session(client_key="ckey", client_secret="csec", blueprint=bp) sess.load_token = mock.Mock(wraps=sess.load_token) assert sess.authorized == False assert sess.load_token.called @responses.activate def test_oauth1session_request(): responses.add(responses.GET, "https://example.com/test") bp = mock.Mock(token=None) sess = OAuth1Session(client_key="ckey", client_secret="csec", blueprint=bp) sess.load_token = mock.Mock(wraps=sess.load_token) sess.get("https://example.com/test") assert sess.load_token.called @responses.activate def test_oauth1session_should_load_token(): responses.add(responses.GET, "https://example.com/test") bp = mock.Mock(token=None) sess = OAuth1Session(client_key="ckey", client_secret="csec", blueprint=bp) sess.load_token = mock.Mock(wraps=sess.load_token) sess.get("https://example.com/test", should_load_token=False) assert not sess.load_token.called def test_oauth2session_authorized(): bp = mock.Mock(token=FAKE_OAUTH2_TOKEN) sess = OAuth2Session(client_id="cid", blueprint=bp) assert sess.authorized == True def test_oauth2session_not_authorized(): bp = mock.Mock(token=None) sess = OAuth2Session(client_id="cid", blueprint=bp) assert sess.authorized == False def test_oauth2session_token(): bp = mock.Mock(token=FAKE_OAUTH2_TOKEN) sess = OAuth2Session(client_id="cid", blueprint=bp) assert sess.token == FAKE_OAUTH2_TOKEN def test_oauth2session_unset_token(): bp = mock.Mock(token=None) sess = OAuth2Session(client_id="cid", blueprint=bp) assert sess.token == None def test_oauth2session_access_token(): bp = mock.Mock(token=FAKE_OAUTH2_TOKEN) sess = OAuth2Session(client_id="cid", blueprint=bp) assert sess.access_token == "deadbeef" def test_oauth2session_unset_access_token(): bp = mock.Mock(token=None) sess = OAuth2Session(client_id="cid", blueprint=bp) assert sess.access_token == None flask-dance-7.1.0/tests/contrib/000077500000000000000000000000001457161140100164715ustar00rootroot00000000000000flask-dance-7.1.0/tests/contrib/test_atlassian.py000066400000000000000000000074561457161140100220750ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.atlassian import atlassian, make_atlassian_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the Atlassian provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_atlassian_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): atlassian_bp = make_atlassian_blueprint( client_id="foo", client_secret="bar", scope="read:jira-user", redirect_to="index", ) assert isinstance(atlassian_bp, OAuth2ConsumerBlueprint) assert atlassian_bp.session.scope == "read:jira-user" assert atlassian_bp.session.base_url == "https://api.atlassian.com/" assert atlassian_bp.session.client_id == "foo" assert atlassian_bp.client_secret == "bar" assert atlassian_bp.authorization_url == "https://auth.atlassian.com/authorize" assert atlassian_bp.token_url == "https://auth.atlassian.com/oauth/token" def test_load_from_config(make_app): app = make_app(redirect_to="index") app.config["ATLASSIAN_OAUTH_CLIENT_ID"] = "foo" app.config["ATLASSIAN_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/atlassian") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" def test_blueprint_factory_scope(): atlassian_bp = make_atlassian_blueprint( client_id="foo", client_secret="bar", scope="customscope" ) assert atlassian_bp.session.scope == "customscope" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://atlassian.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `atlassian` # object will raise an exception with pytest.raises(RuntimeError): atlassian.get("https://atlassian.com") # inside of a request context, `atlassian` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() atlassian.get("https://atlassian.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() atlassian.get("https://atlassian.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" def app_redirect_location(app): with app.test_client() as client: resp = client.get( "/atlassian", base_url="https://a.b.c", follow_redirects=False ) assert resp.status_code == 302 return URLObject(resp.headers["Location"]) def test_default_redirect_params(make_app): app = make_app("foo", "bar") query_dict = app_redirect_location(app).query_dict assert isinstance(query_dict.pop("state"), str) assert query_dict == { "audience": "api.atlassian.com", "client_id": "foo", "redirect_uri": "https://a.b.c/atlassian/authorized", "response_type": "code", } def test_consent(make_app): app = make_app("foo", "bar", reprompt_consent=True) assert app_redirect_location(app).query_dict["prompt"] == "consent" flask-dance-7.1.0/tests/contrib/test_authentiq.py000066400000000000000000000066061457161140100221140ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.authentiq import authentiq, make_authentiq_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the Authentiq provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_authentiq_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory_default(): # Test with connect.authentiq.io aqbp = make_authentiq_blueprint( client_id="foo", client_secret="bar", scope="openid profile", redirect_to="index", ) assert isinstance(aqbp, OAuth2ConsumerBlueprint) assert aqbp.session.scope == "openid profile" assert aqbp.session.base_url == "https://connect.authentiq.io/" assert aqbp.session.client_id == "foo" assert aqbp.client_secret == "bar" assert aqbp.authorization_url == "https://connect.authentiq.io/authorize" assert aqbp.token_url == "https://connect.authentiq.io/token" def test_blueprint_factory_custom(): aqbp = make_authentiq_blueprint( client_id="foo", client_secret="bar", scope="openid profile", redirect_to="index", hostname="local.example.com", ) assert isinstance(aqbp, OAuth2ConsumerBlueprint) assert aqbp.session.scope == "openid profile" assert aqbp.session.base_url == "https://local.example.com/" assert aqbp.session.client_id == "foo" assert aqbp.client_secret == "bar" assert aqbp.authorization_url == "https://local.example.com/authorize" assert aqbp.token_url == "https://local.example.com/token" def test_load_from_config(make_app): app = make_app(redirect_to="index") app.config["AUTHENTIQ_OAUTH_CLIENT_ID"] = "foo" app.config["AUTHENTIQ_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/authentiq") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `authentiq` # object will raise an exception with pytest.raises(RuntimeError): authentiq.get("https://google.com") # inside of a request context, `authentiq` should be a proxy to the # correct blueprint session with app1.test_request_context("/"): app1.preprocess_request() authentiq.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() authentiq.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_azure.py000066400000000000000000000157741457161140100212460ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.azure import azure, make_azure_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the Azure provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_azure_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): azure_bp = make_azure_blueprint( client_id="foo", client_secret="bar", scope="user.read", redirect_to="index" ) assert isinstance(azure_bp, OAuth2ConsumerBlueprint) assert azure_bp.session.scope == "user.read" assert azure_bp.session.base_url == "https://graph.microsoft.com" assert azure_bp.session.client_id == "foo" assert azure_bp.client_secret == "bar" assert ( azure_bp.authorization_url == "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" ) assert ( azure_bp.token_url == "https://login.microsoftonline.com/common/oauth2/v2.0/token" ) assert azure_bp.auto_refresh_url is None def test_blueprint_factory_offline(): azure_bp = make_azure_blueprint( client_id="foo", client_secret="bar", scope=["user.read", "offline_access"], redirect_to="index", ) assert isinstance(azure_bp, OAuth2ConsumerBlueprint) assert azure_bp.session.scope == ["user.read", "offline_access"] assert azure_bp.session.base_url == "https://graph.microsoft.com" assert azure_bp.session.client_id == "foo" assert azure_bp.client_secret == "bar" assert ( azure_bp.authorization_url == "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" ) assert ( azure_bp.token_url == "https://login.microsoftonline.com/common/oauth2/v2.0/token" ) assert azure_bp.auto_refresh_url is azure_bp.token_url def test_blueprint_factory_with_domain_hint(): azure_domain_bp = make_azure_blueprint( client_id="foo", client_secret="bar", scope="user.read", redirect_to="index", domain_hint="Sample Hint", ) assert isinstance(azure_domain_bp, OAuth2ConsumerBlueprint) assert azure_domain_bp.session.scope == "user.read" assert azure_domain_bp.session.base_url == "https://graph.microsoft.com" assert azure_domain_bp.session.client_id == "foo" assert azure_domain_bp.client_secret == "bar" assert azure_domain_bp.authorization_url_params["domain_hint"] == "Sample Hint" assert ( azure_domain_bp.authorization_url == "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" ) assert ( azure_domain_bp.token_url == "https://login.microsoftonline.com/common/oauth2/v2.0/token" ) def test_blueprint_factory_with_login_hint(): azure_domain_bp = make_azure_blueprint( client_id="foo", client_secret="bar", scope="user.read", redirect_to="index", login_hint="Sample Login Hint", ) assert isinstance(azure_domain_bp, OAuth2ConsumerBlueprint) assert azure_domain_bp.session.scope == "user.read" assert azure_domain_bp.session.base_url == "https://graph.microsoft.com" assert azure_domain_bp.session.client_id == "foo" assert azure_domain_bp.client_secret == "bar" assert azure_domain_bp.authorization_url_params["login_hint"] == "Sample Login Hint" assert ( azure_domain_bp.authorization_url == "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" ) assert ( azure_domain_bp.token_url == "https://login.microsoftonline.com/common/oauth2/v2.0/token" ) def test_blueprint_factory_with_prompt(): azure_domain_bp = make_azure_blueprint( client_id="foo", client_secret="bar", scope="user.read", redirect_to="index", prompt="select_account", ) assert isinstance(azure_domain_bp, OAuth2ConsumerBlueprint) assert azure_domain_bp.session.scope == "user.read" assert azure_domain_bp.session.base_url == "https://graph.microsoft.com" assert azure_domain_bp.session.client_id == "foo" assert azure_domain_bp.client_secret == "bar" assert azure_domain_bp.authorization_url_params["prompt"] == "select_account" assert ( azure_domain_bp.authorization_url == "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" ) assert ( azure_domain_bp.token_url == "https://login.microsoftonline.com/common/oauth2/v2.0/token" ) def test_blueprint_factory_with_organization_tenant(): azure_orgs_bp = make_azure_blueprint( client_id="foo", client_secret="bar", scope="user.read", redirect_to="index", tenant="organizations", ) assert isinstance(azure_orgs_bp, OAuth2ConsumerBlueprint) assert azure_orgs_bp.session.scope == "user.read" assert azure_orgs_bp.session.base_url == "https://graph.microsoft.com" assert azure_orgs_bp.session.client_id == "foo" assert azure_orgs_bp.client_secret == "bar" assert ( azure_orgs_bp.authorization_url == "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize" ) assert ( azure_orgs_bp.token_url == "https://login.microsoftonline.com/organizations/oauth2/v2.0/token" ) def test_load_from_config(make_app): app = make_app(redirect_to="index") app.config["AZURE_OAUTH_CLIENT_ID"] = "foo" app.config["AZURE_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/azure") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `azure` object # will raise an exception with pytest.raises(RuntimeError): azure.get("https://google.com") # inside of a request context, `azure` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() azure.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() azure.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_dexcom.py000066400000000000000000000052621457161140100213660ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.dexcom import dexcom, make_dexcom_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the dexcom provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_dexcom_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): dexcom_bp = make_dexcom_blueprint( client_id="foo", client_secret="bar", scope="user:email", redirect_to="index" ) assert isinstance(dexcom_bp, OAuth2ConsumerBlueprint) assert dexcom_bp.session.scope == "user:email" assert dexcom_bp.session.base_url == "https://api.dexcom.com/" assert dexcom_bp.session.client_id == "foo" assert dexcom_bp.client_secret == "bar" assert dexcom_bp.authorization_url == "https://api.dexcom.com/v2/oauth2/login" assert dexcom_bp.token_url == "https://api.dexcom.com/v2/oauth2/token" def test_load_from_config(make_app): app = make_app() app.config["DEXCOM_OAUTH_CLIENT_ID"] = "foo" app.config["DEXCOM_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/dexcom") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `dexcom` object # will raise an exception with pytest.raises(RuntimeError): dexcom.get("https://google.com") # inside of a request context, `dexcom` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() dexcom.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() dexcom.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_digitalocean.py000066400000000000000000000062701457161140100225320ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.digitalocean import digitalocean, make_digitalocean_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the digitalocean provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_digitalocean_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_scope_list_is_valid_with_single_scope(): digitalocean_bp = make_digitalocean_blueprint( client_id="foobar", client_secret="supersecret", scope="read" ) assert digitalocean_bp.session.scope == "read" def test_scope_list_is_converted_to_space_delimited(): digitalocean_bp = make_digitalocean_blueprint( client_id="foobar", client_secret="supersecret", scope="read,write" ) assert digitalocean_bp.session.scope == "read write" def test_blueprint_factory(): digitalocean_bp = make_digitalocean_blueprint( client_id="foobar", client_secret="supersecret" ) assert isinstance(digitalocean_bp, OAuth2ConsumerBlueprint) assert digitalocean_bp.session.client_id == "foobar" assert digitalocean_bp.client_secret == "supersecret" assert digitalocean_bp.token_url == "https://cloud.digitalocean.com/v1/oauth/token" assert ( digitalocean_bp.authorization_url == "https://cloud.digitalocean.com/v1/oauth/authorize" ) @responses.activate def test_load_from_config(make_app): app = make_app() app.config["DIGITALOCEAN_OAUTH_CLIENT_ID"] = "foo" app.config["DIGITALOCEAN_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/digitalocean") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `digitalocean` # object will raise an exception with pytest.raises(RuntimeError): digitalocean.get("https://google.com") # inside of a request context, `digitalocean` should be a proxy to the # correct blueprint session with app1.test_request_context("/"): app1.preprocess_request() digitalocean.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() digitalocean.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_discord.py000066400000000000000000000070761457161140100215430ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.discord import discord, make_discord_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the Discord provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_discord_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): discord_bp = make_discord_blueprint( client_id="foo", client_secret="bar", scope=["identify", "email"], redirect_to="index", ) assert isinstance(discord_bp, OAuth2ConsumerBlueprint) assert discord_bp.session.scope == ["identify", "email"] assert discord_bp.session.base_url == "https://discord.com/" assert discord_bp.session.client_id == "foo" assert discord_bp.client_secret == "bar" assert discord_bp.authorization_url == "https://discord.com/api/oauth2/authorize" assert discord_bp.token_url == "https://discord.com/api/oauth2/token" assert discord_bp.authorization_url_params["prompt"] == "consent" def test_blueprint_factory_with_prompt(): discord_bp = make_discord_blueprint( client_id="foo", client_secret="bar", scope=["identify", "email"], redirect_to="index", prompt=None, ) assert isinstance(discord_bp, OAuth2ConsumerBlueprint) assert discord_bp.session.scope == ["identify", "email"] assert discord_bp.session.base_url == "https://discord.com/" assert discord_bp.session.client_id == "foo" assert discord_bp.client_secret == "bar" assert discord_bp.authorization_url == "https://discord.com/api/oauth2/authorize" assert discord_bp.token_url == "https://discord.com/api/oauth2/token" assert discord_bp.authorization_url_params["prompt"] == None def test_load_from_config(make_app): app = make_app(redirect_to="index") app.config["DISCORD_OAUTH_CLIENT_ID"] = "foo" app.config["DISCORD_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/discord") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), prompt=None, ) # outside of a request context, referencing functions on the `discord` object # will raise an exception with pytest.raises(RuntimeError): discord.get("https://google.com") # inside of a request context, `discord` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() discord.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() discord.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_dropbox.py000066400000000000000000000074741457161140100215730ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.dropbox import dropbox, make_dropbox_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the Dropbox provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_dropbox_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): dropbox_bp = make_dropbox_blueprint(app_key="foo", app_secret="bar") assert isinstance(dropbox_bp, OAuth2ConsumerBlueprint) assert dropbox_bp.session.base_url == "https://api.dropbox.com/2/" assert dropbox_bp.session.client_id == "foo" assert dropbox_bp.client_secret == "bar" assert dropbox_bp.authorization_url == "https://www.dropbox.com/oauth2/authorize" assert dropbox_bp.token_url == "https://api.dropbox.com/oauth2/token" def test_load_from_config(make_app): app = make_app() app.config["DROPBOX_OAUTH_CLIENT_ID"] = "foo" app.config["DROPBOX_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/dropbox") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://dropbox.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `dropbox` object # will raise an exception with pytest.raises(RuntimeError): dropbox.get("https://dropbox.com") # inside of a request context, `dropbox` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() dropbox.get("https://dropbox.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() dropbox.get("https://dropbox.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" def app_redirect_location(app): with app.test_client() as client: resp = client.get("/dropbox", base_url="https://a.b.c", follow_redirects=False) assert resp.status_code == 302 return URLObject(resp.headers["Location"]) def test_default_redirect_params(make_app): app = make_app("foo", "bar") query_dict = app_redirect_location(app).query_dict assert isinstance(query_dict.pop("state"), str) assert query_dict == { "client_id": "foo", "redirect_uri": "https://a.b.c/dropbox/authorized", "response_type": "code", } def test_force_reapprove(make_app): app = make_app("foo", "bar", force_reapprove=True) assert app_redirect_location(app).query_dict["force_reapprove"] == "true" def test_disable_signup(make_app): app = make_app("foo", "bar", disable_signup=True) assert app_redirect_location(app).query_dict["disable_signup"] == "true" def test_require_role(make_app): app = make_app("foo", "bar", require_role="work") assert app_redirect_location(app).query_dict["require_role"] == "work" def test_offline(make_app): app = make_app("foo", "bar", offline=True) assert app_redirect_location(app).query_dict["token_access_type"] == "offline" flask-dance-7.1.0/tests/contrib/test_facebook.py000066400000000000000000000072671457161140100216670ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.facebook import facebook, make_facebook_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the Facebook provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_facebook_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): facebook_bp = make_facebook_blueprint( client_id="foo", client_secret="bar", scope="user:email", redirect_to="index" ) assert isinstance(facebook_bp, OAuth2ConsumerBlueprint) assert facebook_bp.session.scope == "user:email" assert facebook_bp.session.base_url == "https://graph.facebook.com/" assert facebook_bp.session.client_id == "foo" assert facebook_bp.client_secret == "bar" assert facebook_bp.authorization_url == "https://www.facebook.com/dialog/oauth" assert facebook_bp.token_url == "https://graph.facebook.com/oauth/access_token" def test_load_from_config(make_app): app = make_app(redirect_to="index") app.config["FACEBOOK_OAUTH_CLIENT_ID"] = "foo" app.config["FACEBOOK_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/facebook") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `facebook` object # will raise an exception with pytest.raises(RuntimeError): facebook.get("https://google.com") # inside of a request context, `facebook` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() facebook.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() facebook.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" @pytest.mark.parametrize("rerequest", (True, False)) def test_rerequest_declined_scopes(make_app, rerequest): """ Tests that the rerequest_declined_permissions flag in the facebook blueprint sends toggles the header reasking oauth permissions as detailed in the facebook docs https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#reaskperms Tests both that the header is set with the flag (rerequest=True) and that it is missing without the flag (rerequest=False) """ app = make_app(scope="user_posts", rerequest_declined_permissions=rerequest) with app.test_client() as client: resp = client.get("/facebook", base_url="https://a.b.c", follow_redirects=False) assert resp.status_code == 302 location = URLObject(resp.headers["Location"]) if rerequest: assert location.query_dict["auth_type"] == "rerequest" else: assert "auth_type" not in location.query_dict flask-dance-7.1.0/tests/contrib/test_fitbit.py000066400000000000000000000052601457161140100213660ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.fitbit import fitbit, make_fitbit_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the Fitbit provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_fitbit_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): fitbit_bp = make_fitbit_blueprint( client_id="foo", client_secret="bar", scope="user:email", redirect_to="index" ) assert isinstance(fitbit_bp, OAuth2ConsumerBlueprint) assert fitbit_bp.session.scope == "user:email" assert fitbit_bp.session.base_url == "https://api.fitbit.com/" assert fitbit_bp.session.client_id == "foo" assert fitbit_bp.client_secret == "bar" assert fitbit_bp.authorization_url == "https://www.fitbit.com/oauth2/authorize" assert fitbit_bp.token_url == "https://api.fitbit.com/oauth2/token" def test_load_from_config(make_app): app = make_app() app.config["FITBIT_OAUTH_CLIENT_ID"] = "foo" app.config["FITBIT_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/fitbit") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `fitbit` object # will raise an exception with pytest.raises(RuntimeError): fitbit.get("https://google.com") # inside of a request context, `fitbit` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() fitbit.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() fitbit.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_github.py000066400000000000000000000052711457161140100213710ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.github import github, make_github_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the GitHub provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_github_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): github_bp = make_github_blueprint( client_id="foo", client_secret="bar", scope="user:email", redirect_to="index" ) assert isinstance(github_bp, OAuth2ConsumerBlueprint) assert github_bp.session.scope == "user:email" assert github_bp.session.base_url == "https://api.github.com/" assert github_bp.session.client_id == "foo" assert github_bp.client_secret == "bar" assert github_bp.authorization_url == "https://github.com/login/oauth/authorize" assert github_bp.token_url == "https://github.com/login/oauth/access_token" def test_load_from_config(make_app): app = make_app() app.config["GITHUB_OAUTH_CLIENT_ID"] = "foo" app.config["GITHUB_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/github") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `github` object # will raise an exception with pytest.raises(RuntimeError): github.get("https://google.com") # inside of a request context, `github` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() github.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() github.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_gitlab.py000066400000000000000000000125421457161140100213500ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.gitlab import gitlab, make_gitlab_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the GitLab provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.config["SERVER_NAME"] = "a.b.c" app.secret_key = "whatever" blueprint = make_gitlab_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory_default(): # Test with gitlab.com glbp = make_gitlab_blueprint( client_id="foo", client_secret="bar", scope="read_user", redirect_to="index" ) assert isinstance(glbp, OAuth2ConsumerBlueprint) assert glbp.session.scope == "read_user" assert glbp.session.base_url == "https://gitlab.com/api/v4/" assert glbp.session.client_id == "foo" assert glbp.client_secret == "bar" assert glbp.authorization_url == "https://gitlab.com/oauth/authorize" assert glbp.token_url == "https://gitlab.com/oauth/token" def test_blueprint_factory_custom(): glbp = make_gitlab_blueprint( client_id="foo", client_secret="bar", scope="read_user", redirect_to="index", hostname="git.example.com", ) assert isinstance(glbp, OAuth2ConsumerBlueprint) assert glbp.session.scope == "read_user" assert glbp.session.base_url == "https://git.example.com/api/v4/" assert glbp.session.client_id == "foo" assert glbp.client_secret == "bar" assert glbp.authorization_url == "https://git.example.com/oauth/authorize" assert glbp.token_url == "https://git.example.com/oauth/token" def test_load_from_config(make_app): app = make_app() app.config["GITLAB_OAUTH_CLIENT_ID"] = "foo" app.config["GITLAB_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/gitlab") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `gitlab` object # will raise an exception with pytest.raises(RuntimeError): gitlab.get("https://google.com") # inside of a request context, `gitlab` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() gitlab.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() gitlab.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" def test_no_verify_token(make_app, mocker): app = make_app( "foo", "bar", hostname="my-insecure-gitlab.com", storage=MemoryStorage(), verify_tls_certificates=False, ) with responses.RequestsMock() as rsps: mock_on_request = mocker.patch.object( rsps, "_on_request", wraps=rsps._on_request ) rsps.add( responses.POST, "https://my-insecure-gitlab.com/oauth/token", body='{"access_token":"foobar","token_type":"bearer","scope":"admin"}', ) with app.test_client() as client: # reset the session before the request with client.session_transaction() as sess: sess["gitlab_oauth_state"] = "random-string" # make the request resp = client.get( "/gitlab/authorized?code=secret-code&state=random-string", base_url="https://a.b.c", ) # check that `verify=False` in the token request mock_on_request.assert_called() call_verify = mock_on_request.call_args[1].get("verify", True) assert call_verify is False def test_no_verify_api_call(make_app, mocker): app = make_app( "foo", "bar", hostname="my-insecure-gitlab.com", storage=MemoryStorage({"access_token": "fake-token"}), verify_tls_certificates=False, ) with responses.RequestsMock() as rsps: mock_on_request = mocker.patch.object( rsps, "_on_request", wraps=rsps._on_request ) rsps.add( method=responses.GET, url="https://my-insecure-gitlab.com", body="insecure but OK", ) with app.test_request_context("/"): app.preprocess_request() gitlab.get("https://my-insecure-gitlab.com") # check that `verify=False` in the API call mock_on_request.assert_called() call_verify = mock_on_request.call_args[1].get("verify", True) assert call_verify is False flask-dance-7.1.0/tests/contrib/test_google.py000066400000000000000000000147311457161140100213640ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.google import google, make_google_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the Google provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_google_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): google_bp = make_google_blueprint( client_id="foo", client_secret="bar", redirect_to="index" ) assert isinstance(google_bp, OAuth2ConsumerBlueprint) assert google_bp.session.scope == [ "https://www.googleapis.com/auth/userinfo.profile" ] assert google_bp.session.base_url == "https://www.googleapis.com/" assert google_bp.session.client_id == "foo" assert google_bp.client_secret == "bar" assert google_bp.authorization_url == "https://accounts.google.com/o/oauth2/auth" assert google_bp.token_url == "https://accounts.google.com/o/oauth2/token" assert google_bp.auto_refresh_url is None def test_load_from_config(make_app): app = make_app(redirect_to="index") app.config["GOOGLE_OAUTH_CLIENT_ID"] = "foo" app.config["GOOGLE_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/google") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" def test_blueprint_factory_scope(): google_bp = make_google_blueprint( client_id="foo", client_secret="bar", scope="customscope", redirect_to="index" ) assert google_bp.session.scope == "customscope" def test_blueprint_factory_offline(): google_bp = make_google_blueprint( client_id="foo", client_secret="bar", redirect_to="index", offline=True ) assert google_bp.auto_refresh_url == "https://accounts.google.com/o/oauth2/token" def test_blueprint_factory_hosted_domain(): google_bp = make_google_blueprint( client_id="foo", client_secret="bar", redirect_to="index", hosted_domain="example.com", ) assert google_bp.authorization_url_params["hd"] == "example.com" def test_blueprint_factory_rule_kwargs(make_app): app = make_app( client_id="foo", client_secret="bar", redirect_to="index", rule_kwargs={"host": "example2.com"}, ) rules = [ rule for rule in app.url_map.iter_rules() if rule.endpoint.startswith("google.") ] assert all(rule.host == "example2.com" for rule in rules) assert len(rules) == 2 def test_blueprint_factory_no_rule_kwargs(make_app): app = make_app( client_id="foo", client_secret="bar", redirect_to="index", ) rules = [ rule for rule in app.url_map.iter_rules() if rule.endpoint.startswith("google.") ] assert all(rule.host is None for rule in rules) assert len(rules) == 2 @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `google` object # will raise an exception with pytest.raises(RuntimeError): google.get("https://github.com") # inside of a request context, `google` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() google.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() google.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" def test_offline(make_app): app = make_app("foo", "bar", offline=True) with app.test_client() as client: resp = client.get("/google", base_url="https://a.b.c", follow_redirects=False) # check that there is a `access_type=offline` query param in the redirect URL assert resp.status_code == 302 location = URLObject(resp.headers["Location"]) assert location.query_dict["access_type"] == "offline" def test_hd(make_app): app = make_app("foo", "bar", hosted_domain="example.com") with app.test_client() as client: resp = client.get("/google", base_url="https://a.b.c", follow_redirects=False) # check that there is a `hd=example.com` query param in the redirect URL assert resp.status_code == 302 location = URLObject(resp.headers["Location"]) assert location.query_dict["hd"] == "example.com" def test_offline_consent(make_app): app = make_app("foo", "bar", offline=True, reprompt_consent=True) with app.test_client() as client: resp = client.get("/google", base_url="https://a.b.c", follow_redirects=False) assert resp.status_code == 302 location = URLObject(resp.headers["Location"]) assert location.query_dict["access_type"] == "offline" assert location.query_dict["prompt"] == "consent" def test_offline_select_account(make_app): app = make_app("foo", "bar", offline=True, reprompt_select_account=True) with app.test_client() as client: resp = client.get("/google", base_url="https://a.b.c", follow_redirects=False) assert resp.status_code == 302 location = URLObject(resp.headers["Location"]) assert location.query_dict["access_type"] == "offline" assert location.query_dict["prompt"] == "select_account" def test_offline_select_account_and_consent(make_app): app = make_app( "foo", "bar", offline=True, reprompt_consent=True, reprompt_select_account=True ) with app.test_client() as client: resp = client.get("/google", base_url="https://a.b.c", follow_redirects=False) assert resp.status_code == 302 location = URLObject(resp.headers["Location"]) assert location.query_dict["access_type"] == "offline" assert location.query_dict["prompt"] == "consent select_account" flask-dance-7.1.0/tests/contrib/test_heroku.py000066400000000000000000000052441457161140100214040ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.heroku import heroku, make_heroku_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the Heroku provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_heroku_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): heroku_bp = make_heroku_blueprint( client_id="foo", client_secret="bar", scope="global", redirect_to="index" ) assert isinstance(heroku_bp, OAuth2ConsumerBlueprint) assert heroku_bp.session.scope == "global" assert heroku_bp.session.base_url == "https://api.heroku.com/" assert heroku_bp.session.client_id == "foo" assert heroku_bp.client_secret == "bar" assert heroku_bp.authorization_url == "https://id.heroku.com/oauth/authorize" assert heroku_bp.token_url == "https://id.heroku.com/oauth/token" def test_load_from_config(make_app): app = make_app() app.config["HEROKU_OAUTH_CLIENT_ID"] = "foo" app.config["HEROKU_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/heroku") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `heroku` object # will raise an exception with pytest.raises(RuntimeError): heroku.get("https://google.com") # inside of a request context, `heroku` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() heroku.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() heroku.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_jira.py000066400000000000000000000145441457161140100210370ustar00rootroot00000000000000import os import tempfile from unittest import mock import pytest import responses from flask import Flask from oauthlib.oauth1.rfc5849.utils import parse_authorization_header from flask_dance.consumer import OAuth1ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.jira import jira, make_jira_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the JIRA provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_jira_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): jira_bp = make_jira_blueprint( consumer_key="foobar", rsa_key="supersecret", base_url="https://flask.atlassian.net", redirect_to="index", ) assert isinstance(jira_bp, OAuth1ConsumerBlueprint) assert jira_bp.session.base_url == "https://flask.atlassian.net" assert jira_bp.session.auth.client.client_key == "foobar" assert jira_bp.session.auth.client.rsa_key == "supersecret" assert ( jira_bp.request_token_url == "https://flask.atlassian.net/plugins/servlet/oauth/request-token" ) assert ( jira_bp.access_token_url == "https://flask.atlassian.net/plugins/servlet/oauth/access-token" ) assert ( jira_bp.authorization_url == "https://flask.atlassian.net/plugins/servlet/oauth/authorize" ) def test_blueprint_factory_rule_kwargs(make_app): app = make_app( "https://flask.atlassian.net", redirect_to="index", rule_kwargs={"host": "example2.com"}, ) rules = [ rule for rule in app.url_map.iter_rules() if rule.endpoint.startswith("jira.") ] assert all(rule.host == "example2.com" for rule in rules) assert len(rules) == 2 def test_blueprint_factory_no_rule_kwargs(make_app): app = make_app( "https://flask.atlassian.net", redirect_to="index", ) rules = [ rule for rule in app.url_map.iter_rules() if rule.endpoint.startswith("jira.") ] assert all(rule.host is None for rule in rules) assert len(rules) == 2 def test_rsa_key_file(tmp_path): rsa_key = tmp_path / "fake.key" rsa_key.write_text("my-fake-key") jira_bp = make_jira_blueprint( rsa_key=str(rsa_key), base_url="https://flask.atlassian.net" ) assert jira_bp.rsa_key == "my-fake-key" @responses.activate @mock.patch( "oauthlib.oauth1.rfc5849.Client.get_oauth_signature", return_value="fakesig" ) def test_load_from_config(sign_func, make_app): responses.add( responses.POST, "https://flask.atlassian.net/plugins/servlet/oauth/request-token", body="oauth_token=faketoken&oauth_token_secret=fakesecret", ) app = make_app("https://flask.atlassian.net", redirect_to="index") app.config["JIRA_OAUTH_CONSUMER_KEY"] = "foo" app.config["JIRA_OAUTH_RSA_KEY"] = "bar" resp = app.test_client().get("/jira") assert len(responses.calls) > 0 auth_header = dict( parse_authorization_header( responses.calls[0].request.headers["Authorization"].decode("utf-8") ) ) assert auth_header["oauth_consumer_key"] == "foo" sign_func.assert_called() request = sign_func.call_args[0][0] assert dict(request.oauth_params)["oauth_signature"] == "fakesig" @responses.activate @mock.patch( "oauthlib.oauth1.rfc5849.Client.get_oauth_signature", return_value="fakesig" ) def test_content_type(sign_func, make_app): responses.add(responses.GET, "https://flask.atlassian.net/") storage = MemoryStorage( { "oauth_token": "faketoken", "oauth_token_secret": "fakesecret", "oauth_session_handle": "fakehandle", "oauth_expires_in": "157680000", "oauth_authorization_expires_in": "160272000", } ) app = make_app( "https://flask.atlassian.net", rsa_key="fakersa", consumer_key="fakekey", storage=storage, ) @app.route("/test") def api_request(): jira.get("/") return "success" resp = app.test_client().get("/test") assert len(responses.calls) > 0 headers = responses.calls[0].request.headers assert "Content-Type" in headers assert headers["Content-Type"] == b"application/json" @responses.activate def test_context_local(): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = Flask(__name__) jbp1 = make_jira_blueprint( "https://t1.atlassian.com", "foo1", "bar1", redirect_to="url1" ) app1.register_blueprint(jbp1) app2 = Flask(__name__) jbp2 = make_jira_blueprint( "https://t2.atlassian.com", "foo2", "bar2", redirect_to="url2" ) app2.register_blueprint(jbp2) # outside of a request context, referencing functions on the `jira` object # will raise an exception with pytest.raises(RuntimeError): jira.get("https://google.com") # inside of a request context, `jira` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): jbp1.session.auth.client.get_oauth_signature = mock.Mock(return_value="sig1") jbp2.session.auth.client.get_oauth_signature = mock.Mock(return_value="sig2") app1.preprocess_request() jira.get("https://google.com") assert len(responses.calls) > 0 auth_header = dict( parse_authorization_header( responses.calls[0].request.headers["Authorization"].decode("utf-8") ) ) assert auth_header["oauth_consumer_key"] == "foo1" assert auth_header["oauth_signature"] == "sig1" with app2.test_request_context("/"): jbp1.session.auth.client.get_oauth_signature = mock.Mock(return_value="sig1") jbp2.session.auth.client.get_oauth_signature = mock.Mock(return_value="sig2") app2.preprocess_request() jira.get("https://google.com") assert len(responses.calls) > 0 auth_header = dict( parse_authorization_header( responses.calls[1].request.headers["Authorization"].decode("utf-8") ) ) assert auth_header["oauth_consumer_key"] == "foo2" assert auth_header["oauth_signature"] == "sig2" flask-dance-7.1.0/tests/contrib/test_linkedin.py000066400000000000000000000054131457161140100217020ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.linkedin import linkedin, make_linkedin_blueprint @pytest.fixture def make_app(): "A callable to create a Flask app with the LinkedIn provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_linkedin_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): linkedin_bp = make_linkedin_blueprint( client_id="foo", client_secret="bar", scope="user:email", redirect_to="index" ) assert isinstance(linkedin_bp, OAuth2ConsumerBlueprint) assert linkedin_bp.session.scope == "user:email" assert linkedin_bp.session.base_url == "https://api.linkedin.com/v2/" assert linkedin_bp.session.client_id == "foo" assert linkedin_bp.client_secret == "bar" assert ( linkedin_bp.authorization_url == "https://www.linkedin.com/oauth/v2/authorization" ) assert linkedin_bp.token_url == "https://www.linkedin.com/oauth/v2/accessToken" def test_load_from_config(make_app): app = make_app() app.config["LINKEDIN_OAUTH_CLIENT_ID"] = "foo" app.config["LINKEDIN_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/linkedin") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `linkedin` object # will raise an exception with pytest.raises(RuntimeError): linkedin.get("https://google.com") # inside of a request context, `linkedin` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() linkedin.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() linkedin.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_meetup.py000066400000000000000000000055451457161140100214120ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.meetup import make_meetup_blueprint, meetup @pytest.fixture def make_app(): "A callable to create a Flask app with the Meetup provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_meetup_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): meetup_bp = make_meetup_blueprint(key="foo", secret="bar") assert isinstance(meetup_bp, OAuth2ConsumerBlueprint) assert meetup_bp.session.scope == ["basic"] assert meetup_bp.session.base_url == "https://api.meetup.com/2/" assert meetup_bp.session.client_id == "foo" assert meetup_bp.client_secret == "bar" assert meetup_bp.authorization_url == "https://secure.meetup.com/oauth2/authorize" assert meetup_bp.token_url == "https://secure.meetup.com/oauth2/access" assert meetup_bp.token_url_params == {"include_client_id": True} def test_load_from_config(make_app): app = make_app() app.config["MEETUP_OAUTH_CLIENT_ID"] = "foo" app.config["MEETUP_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/meetup") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" def test_blueprint_factory_scope(): meetup_bp = make_meetup_blueprint(key="foo", secret="bar", scope="customscope") assert meetup_bp.session.scope == "customscope" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://meetup.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `meetup` object # will raise an exception with pytest.raises(RuntimeError): meetup.get("https://meetup.com") # inside of a request context, `meetup` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() meetup.get("https://meetup.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() meetup.get("https://meetup.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_nylas.py000066400000000000000000000053001457161140100212260ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.nylas import make_nylas_blueprint, nylas @pytest.fixture def make_app(): "A callable to create a Flask app with the Nylas provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_nylas_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): nylas_bp = make_nylas_blueprint( client_id="foo", client_secret="bar", redirect_to="index" ) assert isinstance(nylas_bp, OAuth2ConsumerBlueprint) assert nylas_bp.session.scope == "email" assert nylas_bp.session.base_url == "https://api.nylas.com/" assert nylas_bp.session.client_id == "foo" assert nylas_bp.client_secret == "bar" assert nylas_bp.authorization_url == "https://api.nylas.com/oauth/authorize" assert nylas_bp.token_url == "https://api.nylas.com/oauth/token" assert nylas_bp.token_url_params == {"include_client_id": True} def test_load_from_config(make_app): app = make_app() app.config["NYLAS_OAUTH_CLIENT_ID"] = "foo" app.config["NYLAS_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/nylas") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `nylas` object # will raise an exception with pytest.raises(RuntimeError): nylas.get("https://google.com") # inside of a request context, `nylas` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() nylas.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() nylas.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_orcid.py000066400000000000000000000064171457161140100212120ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.orcid import make_orcid_blueprint, orcid @pytest.fixture def make_app(): "A callable to create a Flask app with the ORCID provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_orcid_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): orcid_bp = make_orcid_blueprint( client_id="foo", client_secret="bar", scope="user:email", redirect_to="index" ) assert isinstance(orcid_bp, OAuth2ConsumerBlueprint) assert orcid_bp.session.scope == "user:email" assert orcid_bp.session.base_url == "https://api.orcid.org" assert orcid_bp.session.client_id == "foo" assert orcid_bp.client_secret == "bar" assert orcid_bp.authorization_url == "https://orcid.org/oauth/authorize" assert orcid_bp.token_url == "https://orcid.org/oauth/token" def test_sandbox_blueprint_factory(): orcid_bp = make_orcid_blueprint( client_id="foo", client_secret="bar", scope="user:email", redirect_to="index", sandbox=True, ) assert isinstance(orcid_bp, OAuth2ConsumerBlueprint) assert orcid_bp.session.scope == "user:email" assert orcid_bp.session.base_url == "https://api.sandbox.orcid.org" assert orcid_bp.session.client_id == "foo" assert orcid_bp.client_secret == "bar" assert orcid_bp.authorization_url == "https://sandbox.orcid.org/oauth/authorize" assert orcid_bp.token_url == "https://sandbox.orcid.org/oauth/token" def test_load_from_config(make_app): app = make_app() app.config["ORCID_OAUTH_CLIENT_ID"] = "foo" app.config["ORCID_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/orcid") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `orcid` object # will raise an exception with pytest.raises(RuntimeError): orcid.get("https://google.com") # inside of a request context, `orcid` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() orcid.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() orcid.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_osm.py000066400000000000000000000052251457161140100207040ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.osm import make_osm_blueprint, osm @pytest.fixture def make_app(): "A callable to create a Flask app with the OpenStreetMap provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_osm_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): osm_bp = make_osm_blueprint( client_id="foo", client_secret="bar", scope="user:email", redirect_to="index" ) assert isinstance(osm_bp, OAuth2ConsumerBlueprint) assert osm_bp.session.scope == "user:email" assert osm_bp.session.base_url == "https://www.openstreetmap.org/api/0.6/" assert osm_bp.session.client_id == "foo" assert osm_bp.client_secret == "bar" assert osm_bp.authorization_url == "https://www.openstreetmap.org/oauth2/authorize" assert osm_bp.token_url == "https://www.openstreetmap.org/oauth2/token" def test_load_from_config(make_app): app = make_app() app.config["OSM_OAUTH_CLIENT_ID"] = "foo" app.config["OSM_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/osm") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `osm` object # will raise an exception with pytest.raises(RuntimeError): osm.get("https://google.com") # inside of a request context, `osm` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() osm.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() osm.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_reddit.py000066400000000000000000000066241457161140100213650ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.reddit import make_reddit_blueprint, reddit @pytest.fixture def make_app(): "A callable to create a Flask app with the Reddit provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_reddit_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): reddit_bp = make_reddit_blueprint( client_id="foo", client_secret="bar", scope="identity", redirect_to="index" ) assert isinstance(reddit_bp, OAuth2ConsumerBlueprint) assert reddit_bp.session.scope == "identity" assert reddit_bp.session.base_url == "https://oauth.reddit.com/" assert reddit_bp.session.client_id == "foo" assert reddit_bp.client_secret == "bar" assert reddit_bp.authorization_url == "https://www.reddit.com/api/v1/authorize" assert reddit_bp.token_url == "https://www.reddit.com/api/v1/access_token" def test_blueprint_factory_with_permanent_token(): reddit_bp = make_reddit_blueprint( client_id="foo", client_secret="bar", scope="identity", redirect_to="index", permanent=True, ) assert isinstance(reddit_bp, OAuth2ConsumerBlueprint) assert reddit_bp.session.scope == "identity" assert reddit_bp.session.base_url == "https://oauth.reddit.com/" assert reddit_bp.session.client_id == "foo" assert reddit_bp.client_secret == "bar" assert reddit_bp.authorization_url == "https://www.reddit.com/api/v1/authorize" assert reddit_bp.token_url == "https://www.reddit.com/api/v1/access_token" assert reddit_bp.authorization_url_params["duration"] == "permanent" def test_load_from_config(make_app): app = make_app() app.config["REDDIT_OAUTH_CLIENT_ID"] = "foo" app.config["REDDIT_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/reddit") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `reddit` object # will raise an exception with pytest.raises(RuntimeError): reddit.get("https://google.com") # inside of a request context, `reddit` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() reddit.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() reddit.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_salesforce.py000066400000000000000000000125251457161140100222350ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.salesforce import make_salesforce_blueprint, salesforce @pytest.fixture def make_app(): "A callable to create a Flask app with the Salesforce provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_salesforce_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory_default(): salesforce_bp = make_salesforce_blueprint( client_id="foo", client_secret="bar", scope="api", redirect_to="index", ) assert isinstance(salesforce_bp, OAuth2ConsumerBlueprint) assert salesforce_bp.session.scope == "api" assert salesforce_bp.session.base_url == "https://login.salesforce.com/" assert salesforce_bp.session.client_id == "foo" assert salesforce_bp.client_secret == "bar" assert ( salesforce_bp.authorization_url == "https://login.salesforce.com/services/oauth2/authorize" ) assert ( salesforce_bp.token_url == "https://login.salesforce.com/services/oauth2/token" ) def test_blueprint_factory_sandbox(): salesforce_bp = make_salesforce_blueprint( client_id="foo", client_secret="bar", scope="api", redirect_to="index", is_sandbox=True, ) assert isinstance(salesforce_bp, OAuth2ConsumerBlueprint) assert salesforce_bp.session.scope == "api" assert salesforce_bp.session.base_url == "https://test.salesforce.com/" assert salesforce_bp.session.client_id == "foo" assert salesforce_bp.client_secret == "bar" assert ( salesforce_bp.authorization_url == "https://test.salesforce.com/services/oauth2/authorize" ) assert ( salesforce_bp.token_url == "https://test.salesforce.com/services/oauth2/token" ) def test_blueprint_factory_custom(): salesforce_bp = make_salesforce_blueprint( client_id="foo", client_secret="bar", scope="api", redirect_to="index", hostname="example.my.salesforce.com", ) assert isinstance(salesforce_bp, OAuth2ConsumerBlueprint) assert salesforce_bp.session.scope == "api" assert salesforce_bp.session.base_url == "https://example.my.salesforce.com/" assert salesforce_bp.session.client_id == "foo" assert salesforce_bp.client_secret == "bar" assert ( salesforce_bp.authorization_url == "https://example.my.salesforce.com/services/oauth2/authorize" ) assert ( salesforce_bp.token_url == "https://example.my.salesforce.com/services/oauth2/token" ) def test_load_from_config(make_app): app = make_app(redirect_to="index") app.config["SALESFORCE_OAUTH_CLIENT_ID"] = "foo" app.config["SALESFORCE_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/salesforce") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" def test_blueprint_factory_scope(): salesforce_bp = make_salesforce_blueprint( client_id="foo", client_secret="bar", scope="customscope" ) assert salesforce_bp.session.scope == "customscope" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://salesforce.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `salesforce` # object will raise an exception with pytest.raises(RuntimeError): salesforce.get("https://salesforce.com") # inside of a request context, `salesforce` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() salesforce.get("https://salesforce.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() salesforce.get("https://salesforce.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" def app_redirect_location(app): with app.test_client() as client: resp = client.get( "/salesforce", base_url="https://a.b.c", follow_redirects=False ) assert resp.status_code == 302 return URLObject(resp.headers["Location"]) def test_default_redirect_params(make_app): app = make_app("foo", "bar") query_dict = app_redirect_location(app).query_dict assert isinstance(query_dict.pop("state"), str) assert query_dict == { "client_id": "foo", "redirect_uri": "https://a.b.c/salesforce/authorized", "response_type": "code", } def test_consent(make_app): app = make_app("foo", "bar", reprompt_consent=True) assert app_redirect_location(app).query_dict["prompt"] == "consent" flask-dance-7.1.0/tests/contrib/test_slack.py000066400000000000000000000165111457161140100212030ustar00rootroot00000000000000from urllib.parse import parse_qs import pytest import requests_oauthlib import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.slack import make_slack_blueprint, slack @pytest.fixture def make_app(): "A callable to create a Flask app with the Slack provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_slack_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): slack_bp = make_slack_blueprint( client_id="foo", client_secret="bar", scope=["identity", "im:write"], redirect_to="index", ) assert isinstance(slack_bp, OAuth2ConsumerBlueprint) assert slack_bp.session.scope == ["identity", "im:write"] assert slack_bp.session.base_url == "https://slack.com/api/" assert slack_bp.session.client_id == "foo" assert slack_bp.client_secret == "bar" assert slack_bp.authorization_url == "https://slack.com/oauth/authorize" assert slack_bp.token_url == "https://slack.com/api/oauth.access" def test_blueprint_factory_with_subdomain(): slack_bp = make_slack_blueprint( client_id="foo", client_secret="bar", scope=["identity", "im:write"], redirect_to="index", subdomain="my-team", ) assert slack_bp.authorization_url == "https://my-team.slack.com/oauth/authorize" assert slack_bp.token_url == "https://slack.com/api/oauth.access" def test_load_from_config(make_app): app = make_app() app.config["SLACK_OAUTH_CLIENT_ID"] = "foo" app.config["SLACK_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/slack") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://slack.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `slack` object # will raise an exception with pytest.raises(RuntimeError): slack.get("https://slack.com") # inside of a request context, `slack` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() slack.get("https://slack.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() slack.get("https://slack.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" @responses.activate def test_auto_token_get(make_app): responses.add(responses.GET, "https://slack.com/api/chat.postMessage") app = make_app( client_id="foo", client_secret="bar", storage=MemoryStorage({"access_token": "abcde"}), ) with app.test_request_context("/"): app.preprocess_request() resp = slack.get( "chat.postMessage", data={"channel": "#general", "text": "ping", "icon_emoji": ":robot_face:"}, ) request_data = parse_qs(resp.request.body) assert request_data["channel"] == ["#general"] assert request_data["text"] == ["ping"] assert request_data["icon_emoji"] == [":robot_face:"] # the `token` parameter should have been automatically added assert request_data["token"] == ["abcde"] @responses.activate def test_auto_token_post(make_app): responses.add(responses.POST, "https://slack.com/api/chat.postMessage") app = make_app( client_id="foo", client_secret="bar", storage=MemoryStorage({"access_token": "abcde"}), ) with app.test_request_context("/"): app.preprocess_request() resp = slack.post( "chat.postMessage", data={"channel": "#general", "text": "ping", "icon_emoji": ":robot_face:"}, ) request_data = parse_qs(resp.request.body) assert request_data["channel"] == ["#general"] assert request_data["text"] == ["ping"] assert request_data["icon_emoji"] == [":robot_face:"] # the `token` parameter should have been automatically added assert request_data["token"] == ["abcde"] @responses.activate def test_auto_token_post_no_token(make_app): responses.add(responses.POST, "https://slack.com/api/chat.postMessage") app = make_app(client_id="foo", client_secret="bar") with app.test_request_context("/"): app.preprocess_request() resp = slack.post( "chat.postMessage", data={"channel": "#general", "text": "ping", "icon_emoji": ":robot_face:"}, ) request_data = parse_qs(resp.request.body) assert request_data["channel"] == ["#general"] assert request_data["text"] == ["ping"] assert request_data["icon_emoji"] == [":robot_face:"] assert "token" not in request_data url = URLObject(resp.request.url) assert "token" not in url.query_dict @responses.activate def test_override_token_get(make_app): responses.add(responses.GET, "https://slack.com/api/chat.postMessage") app = make_app( client_id="foo", client_secret="bar", storage=MemoryStorage({"access_token": "abcde"}), ) with app.test_request_context("/"): app.preprocess_request() resp = slack.get( "chat.postMessage", data={ "token": "xyz", "channel": "#general", "text": "ping", "icon_emoji": ":robot_face:", }, ) request_data = parse_qs(resp.request.body) assert request_data["token"] == ["xyz"] assert request_data["channel"] == ["#general"] assert request_data["text"] == ["ping"] assert request_data["icon_emoji"] == [":robot_face:"] # should not be present in URL url = URLObject(resp.request.url) assert "token" not in url.query_dict @responses.activate def test_override_token_post(make_app): responses.add(responses.POST, "https://slack.com/api/chat.postMessage") app = make_app( client_id="foo", client_secret="bar", storage=MemoryStorage({"access_token": "abcde"}), ) with app.test_request_context("/"): app.preprocess_request() resp = slack.post( "chat.postMessage", data={ "token": "xyz", "channel": "#general", "text": "ping", "icon_emoji": ":robot_face:", }, ) request_data = parse_qs(resp.request.body) assert request_data["token"] == ["xyz"] assert request_data["channel"] == ["#general"] assert request_data["text"] == ["ping"] assert request_data["icon_emoji"] == [":robot_face:"] # should not be present url = URLObject(resp.request.url) assert "token" not in url.query_dict flask-dance-7.1.0/tests/contrib/test_spotify.py000066400000000000000000000053571457161140100216110ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.spotify import make_spotify_blueprint, spotify @pytest.fixture def make_app(): "A callable to create a Flask app with the Spotify provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_spotify_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): spotify_bp = make_spotify_blueprint( client_id="foo", client_secret="bar", scope="user-read-private", redirect_to="index", ) assert isinstance(spotify_bp, OAuth2ConsumerBlueprint) assert spotify_bp.session.scope == "user-read-private" assert spotify_bp.session.base_url == "https://api.spotify.com" assert spotify_bp.session.client_id == "foo" assert spotify_bp.client_secret == "bar" assert spotify_bp.authorization_url == "https://accounts.spotify.com/authorize" assert spotify_bp.token_url == "https://accounts.spotify.com/api/token" def test_load_from_config(make_app): app = make_app() app.config["SPOTIFY_OAUTH_CLIENT_ID"] = "foo" app.config["SPOTIFY_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/spotify") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `spotify` object # will raise an exception with pytest.raises(RuntimeError): spotify.get("https://google.com") # inside of a request context, `spotify` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() spotify.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() spotify.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_strava.py000066400000000000000000000053171457161140100214100ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.strava import make_strava_blueprint, strava @pytest.fixture def make_app(): "A callable to create a Flask app with the Strava provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_strava_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): strava_bp = make_strava_blueprint( client_id="foo", client_secret="bar", scope="identity", redirect_to="index" ) assert isinstance(strava_bp, OAuth2ConsumerBlueprint) assert strava_bp.session.scope == "identity" assert strava_bp.session.base_url == "https://www.strava.com/api/v3" assert strava_bp.session.client_id == "foo" assert strava_bp.client_secret == "bar" assert ( strava_bp.authorization_url == "https://www.strava.com/api/v3/oauth/authorize" ) assert strava_bp.token_url == "https://www.strava.com/api/v3/oauth/token" def test_load_from_config(make_app): app = make_app() app.config["STRAVA_OAUTH_CLIENT_ID"] = "foo" app.config["STRAVA_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/strava") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `strava` object # will raise an exception with pytest.raises(RuntimeError): strava.get("https://google.com") # inside of a request context, `strava` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() strava.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" with app2.test_request_context("/"): app2.preprocess_request() strava.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" flask-dance-7.1.0/tests/contrib/test_twitch.py000066400000000000000000000056521457161140100214140ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.twitch import make_twitch_blueprint, twitch @pytest.fixture def make_app(): "A callable to create a Flask app with the Twitch provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_twitch_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): scope = [ "user:read:subscriptions", "channel:read:subscriptions", "user:read:subscriptions", ] twitch_bp = make_twitch_blueprint( client_id="foo", client_secret="bar", scope=scope, redirect_to="index", ) assert isinstance(twitch_bp, OAuth2ConsumerBlueprint) assert twitch_bp.session.scope == scope assert twitch_bp.session.base_url == "https://api.twitch.tv/helix/" assert twitch_bp.session.client_id == "foo" assert twitch_bp.client_secret == "bar" assert twitch_bp.authorization_url == "https://id.twitch.tv/oauth2/authorize" assert twitch_bp.token_url == "https://id.twitch.tv/oauth2/token" def test_load_from_config(make_app): app = make_app() app.config["TWITCH_OAUTH_CLIENT_ID"] = "foo" app.config["TWITCH_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/twitch") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `twitch` object # will raise an exception with pytest.raises(RuntimeError): twitch.get("https://google.com") # inside of a request context, `twitch` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() twitch.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Bearer app1" assert request.headers["client-id"] == "foo1" with app2.test_request_context("/"): app2.preprocess_request() twitch.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Bearer app2" assert request.headers["client-id"] == "foo2" flask-dance-7.1.0/tests/contrib/test_zoho.py000066400000000000000000000050301457161140100210570ustar00rootroot00000000000000import pytest import responses from flask import Flask from urlobject import URLObject from flask_dance.consumer import OAuth2ConsumerBlueprint from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.zoho import make_zoho_blueprint, zoho @pytest.fixture def make_app(): "A callable to create a Flask app with the Zoho provider" def _make_app(*args, **kwargs): app = Flask(__name__) app.secret_key = "whatever" blueprint = make_zoho_blueprint(*args, **kwargs) app.register_blueprint(blueprint) return app return _make_app def test_blueprint_factory(): zoho_bp = make_zoho_blueprint(client_id="foobar", client_secret="supersecret") assert isinstance(zoho_bp, OAuth2ConsumerBlueprint) assert zoho_bp.session.client_id == "foobar" assert zoho_bp.client_secret == "supersecret" assert zoho_bp.token_url == "https://accounts.zoho.com/oauth/v2/token" assert zoho_bp.authorization_url == "https://accounts.zoho.com/oauth/v2/auth" @responses.activate def test_load_from_config(make_app): app = make_app() app.config["ZOHO_OAUTH_CLIENT_ID"] = "foo" app.config["ZOHO_OAUTH_CLIENT_SECRET"] = "bar" resp = app.test_client().get("/zoho") url = resp.headers["Location"] client_id = URLObject(url).query.dict.get("client_id") assert client_id == "foo" @responses.activate def test_context_local(make_app): responses.add(responses.GET, "https://google.com") # set up two apps with two different set of auth tokens app1 = make_app( "foo1", "bar1", redirect_to="url1", storage=MemoryStorage({"access_token": "app1"}), ) app2 = make_app( "foo2", "bar2", redirect_to="url2", storage=MemoryStorage({"access_token": "app2"}), ) # outside of a request context, referencing functions on the `zoho` object # will raise an exception with pytest.raises(RuntimeError): zoho.get("https://google.com") # inside of a request context, `zoho` should be a proxy to the correct # blueprint session with app1.test_request_context("/"): app1.preprocess_request() zoho.get("https://google.com") request = responses.calls[0].request assert request.headers["Authorization"] == "Zoho-oauthtoken app1" with app2.test_request_context("/"): app2.preprocess_request() zoho.get("https://google.com") request = responses.calls[1].request assert request.headers["Authorization"] == "Zoho-oauthtoken app2" flask-dance-7.1.0/tests/fixtures/000077500000000000000000000000001457161140100167025ustar00rootroot00000000000000flask-dance-7.1.0/tests/fixtures/cassettes/000077500000000000000000000000001457161140100207005ustar00rootroot00000000000000flask-dance-7.1.0/tests/fixtures/cassettes/test_home_page.json000066400000000000000000000050561457161140100245640ustar00rootroot00000000000000{"http_interactions": [{"request": {"body": {"encoding": "utf-8", "string": ""}, "headers": {"User-Agent": ["python-requests/2.21.0"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "Connection": ["keep-alive"], "Authorization": ["Bearer "]}, "method": "GET", "uri": "https://api.github.com/user"}, "response": {"body": {"encoding": "utf-8", "base64_string": "H4sIAAAAAAAAA51U24rbMBT8FaPnbBw7lyaC0Avbl9KELmwv7EuQbMXWIktGkh2yYf+9I9tdttlCwWCwOZ6ZMz5H4wtRppCaUOKkxkNxMurIzZlMiMwJTebpfLmcEG1ycQgFsru9W/34tVfZ4+en3eMu2d9ttwCzlnlmD41VwJTe147GcV9082khfdnwxgmbGe2F9tPMVHET9/Lv2+0CEoUdRLo+KFyJ1XLQ6ckQc/Eb06Wv1JWLvnlHegM/GqXMCUrXzv/fLH7hwmr/jPGN1gH3EhtfCgwRn/YcBiKdH2Os413icMPOgpLDbqzIR5gbmLB20nB1ia2oTSfZcJdZWXtp9BiTf/GhZ2zBtHxiY/XAd5AJ9sbY6Xjgixanc4xAT7zEtZUty85hRFZkQrYY+2jRKwVo+nMtkI7vOCJhCdKLA8urEOAjU04gqawKgFvWyjz6xJqqMCoHFMe/ZvpMqG6UmhCO2AOWBxgfUCEiQCqTdVvA64+V88LmrJpE+69hOhWTIeAd7cO/yKW0gnEFC942sMOlAf6n4FGOKShTCxsxnUdeZKWWGVORt0ziaE2jb2dfGt29/YI/QX+6Io5MRFK7GsJRAXEf0VIw62mHHCrOcDqFwbrhSmaHfps0Xa1eSl0o8ENbvfuTVqSe0HSdvEovoYs1RhW6YG3Mw3o6m21ucKXr+2RDkzVNNw9o1NT5a0wCzOImWd2nM7pc0NnsgTz/Bj2qWQhbBQAA", "string": ""}, "headers": {"Date": ["Wed, 17 Apr 2019 12:00:19 GMT"], "Content-Type": ["application/json; charset=utf-8"], "Transfer-Encoding": ["chunked"], "Server": ["GitHub.com"], "Status": ["200 OK"], "X-RateLimit-Limit": ["5000"], "X-RateLimit-Remaining": ["4999"], "X-RateLimit-Reset": ["1555506019"], "Cache-Control": ["private, max-age=60, s-maxage=60"], "Vary": ["Accept, Authorization, Cookie, X-GitHub-OTP", "Accept-Encoding"], "ETag": ["W/\"2ba8f67d5424c7668702044d47d32a4e\""], "Last-Modified": ["Tue, 16 Apr 2019 20:54:00 GMT"], "X-OAuth-Scopes": [""], "X-Accepted-OAuth-Scopes": [""], "X-GitHub-Media-Type": ["github.v3; format=json"], "Access-Control-Expose-Headers": ["ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type"], "Access-Control-Allow-Origin": ["*"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "X-Frame-Options": ["deny"], "X-Content-Type-Options": ["nosniff"], "X-XSS-Protection": ["1; mode=block"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Security-Policy": ["default-src 'none'"], "Content-Encoding": ["gzip"], "X-GitHub-Request-Id": ["F2D1:3BDA:72250A:8B428A:5CB71552"]}, "status": {"code": 200, "message": "OK"}, "url": "https://api.github.com/user"}, "recorded_at": "2019-04-17T12:00:19"}], "recorded_with": "betamax/0.8.1"} flask-dance-7.1.0/tests/fixtures/test_pytest.py000066400000000000000000000027461457161140100216540ustar00rootroot00000000000000import os import flask import pytest from flask_dance.consumer.storage import MemoryStorage from flask_dance.contrib.github import github, make_github_blueprint betamax = pytest.importorskip("betamax") GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_OAUTH_ACCESS_TOKEN", "fake-token") current_dir = os.path.dirname(__file__) with betamax.Betamax.configure() as config: config.cassette_library_dir = os.path.join(current_dir, "cassettes") config.define_cassette_placeholder("", GITHUB_ACCESS_TOKEN) pytestmark = pytest.mark.install_required @pytest.fixture def app(): _app = flask.Flask(__name__) _app.secret_key = "secret" github_bp = make_github_blueprint( storage=MemoryStorage({"access_token": GITHUB_ACCESS_TOKEN}) ) _app.register_blueprint(github_bp, url_prefix="/login") @_app.route("/") def index(): if not github.authorized: return flask.redirect(flask.url_for("github.login")) resp = github.get("/user") assert resp.ok return "You are @{login} on GitHub".format(login=resp.json()["login"]) return _app @pytest.fixture def flask_dance_sessions(): return github @pytest.mark.usefixtures("betamax_record_flask_dance") def test_home_page(app): with app.test_client() as client: response = client.get("/", base_url="https://example.com") assert response.status_code == 200 text = response.get_data(as_text=True) assert text == "You are @singingwolfboy on GitHub" flask-dance-7.1.0/tests/test_utils.py000066400000000000000000000012561457161140100176060ustar00rootroot00000000000000import pytest from flask_dance.utils import FakeCache, first, getattrd def test_first(): assert first([1, 2, 3]) == 1 assert first([None, 2, 3]) == 2 assert first([None, 0, False, [], {}]) == None assert first([None, 0, False, [], {}], default=42) == 42 first([1, 1, 3, 4, 5], key=lambda x: x % 2 == 0) == 4 class C: d = "foo" class B: C = C class A: B = B def test_getattrd(): assert A.B.C.d == "foo" assert getattrd(A, "B.C.d") == "foo" assert getattrd(A, "B") == B assert getattrd(A, "B", default=42) == B assert getattrd(A, "Q", default=42) == 42 with pytest.raises(AttributeError): assert getattrd(A, "Q")