├── .env.example ├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── Procfile ├── README.rst ├── app.json ├── dev-requirements.txt ├── github.py ├── requirements.txt └── tests ├── README.rst ├── cassettes └── test_index_authorized.json ├── conftest.py └── test_github.py /.env.example: -------------------------------------------------------------------------------- 1 | FLASK_APP=github.py 2 | GITHUB_OAUTH_CLIENT_ID= 3 | GITHUB_OAUTH_CLIENT_SECRET= 4 | OAUTHLIB_INSECURE_TRANSPORT=true 5 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | pytest: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["2.x", "3.x"] 16 | name: "pytest: Python ${{ matrix.python-version }}" 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install dependencies 25 | run: >- 26 | pip install 27 | -r requirements.txt 28 | -r dev-requirements.txt 29 | 30 | - name: Run pytest 31 | run: pytest 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .env 3 | venv 4 | .vscode 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2015-2021 David Baumgold 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn github:app --log-file=- 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-Dance Example App: GitHub Edition 2 | ======================================= 3 | 4 | This repository provides an example of how to use `Flask-Dance`_ to connect 5 | to `GitHub`_ as an OAuth client. The example code is in ``github.py`` -- 6 | all the other files in this repository are secondary. You can run this example 7 | code locally, or deploy it to Heroku for free to see how it runs in a 8 | production-style environment. 9 | 10 | Heroku Installation 11 | ``````````````````` 12 | `Heroku`_ is a great way to get up and running fast, and you don't even need 13 | to open the terminal! 14 | 15 | Step 1: Deploy to Heroku 16 | ------------------------ 17 | It's easy, and it's free! Just click on this button: 18 | 19 | |heroku-deploy| 20 | 21 | You can leave all the fields at their default values: we'll fill them in later. 22 | The only thing that matters right now is the app name, and Heroku will 23 | autogenerate a name for you if you leave that field blank. Using an 24 | autogenerated name is perfectly fine, just take note of what it is. 25 | 26 | Note that your app isn't functional yet, and if you try to visit it right now, 27 | you'll end up at a GitHub 404 page. That's OK, we're not done yet! 28 | 29 | Step 2: Get OAuth credentials from GitHub 30 | ----------------------------------------- 31 | Visit https://github.com/settings/applications/new to register an 32 | application on GitHub. In order to register the application, you'll need that 33 | app name from Heroku. The GitHub application's authorization callback URL 34 | must be ``https://APPNAME.herokuapp.com/login/github/authorized``. For example, 35 | if Heroku assigned you an app name of ``peaceful-lake``, your authorization 36 | callback URL must be 37 | ``https://peaceful-lake.herokuapp.com/login/github/authorized``. 38 | 39 | Once you've registered your application on GitHub, GitHub will give you a 40 | client ID and client secret, which we'll use in the next step. 41 | 42 | Step 3: Give OAuth credentials to your app on Heroku 43 | ---------------------------------------------------- 44 | Go to Heroku and visit the settings page for your app. (You can get there from 45 | your Heroku dashboard, or by clicking on the "Manage App" button after the 46 | deploy step is finished.) On that page, there should be a section called 47 | "Config Variables" where you can manage the config vars for your application. 48 | You'll need click the "Reveal Config Vars" button to see which variables 49 | are available, and then the "Edit" button to allow you to change these 50 | variables. 51 | 52 | Take the client ID you got from GitHub, and paste it into the "VALUE" field 53 | next to the ``GITHUB_OAUTH_CLIENT_ID`` field, replacing the dummy value that 54 | was there before. Similarly, take the client secret you got from GitHub, 55 | and paste it into the "VALUE" field next to the ``GITHUB_OAUTH_CLIENT_SECRET`` 56 | field, replacing the dummy value that was there before. 57 | Click the "Save" button when you're done. 58 | 59 | Step 4: Visit your app and login with GitHub! 60 | --------------------------------------------- 61 | Your app name from Heroku will determine the URL that your app is running on: 62 | the URL will be ``https://APPNAME.herokuapp.com``. For example, if Heroku 63 | assigned you an app name of ``peaceful-lake``, your app will be available at 64 | ``https://peaceful-lake.herokuapp.com``. Visit that URL, and you should 65 | immediately be redirected to login with GitHub! 66 | 67 | Local Installation 68 | `````````````````` 69 | If you'd prefer to run this locally on your computer, you can do that as well. 70 | 71 | Step 1: Get OAuth credentials from GitHub 72 | ----------------------------------------- 73 | Visit https://github.com/settings/applications/new to register an 74 | application on GitHub. You must set the application's authorization 75 | callback URL to ``http://localhost:5000/login/github/authorized``. 76 | 77 | Once you've registered your application on GitHub, GitHub will give you a 78 | client ID and client secret, which we'll use in step 3. 79 | 80 | Step 2: Install code and dependencies 81 | ------------------------------------- 82 | Run the following commands on your computer:: 83 | 84 | git clone https://github.com/singingwolfboy/flask-dance-github.git 85 | cd flask-dance-github 86 | python3 -m venv venv 87 | source venv/bin/activate 88 | pip install -r requirements.txt 89 | 90 | These commands will clone this git repository onto your computer, 91 | create a `virtual environment`_ for this project, activate it, and install 92 | the dependencies listed in ``requirements.txt``. 93 | 94 | Step 3: Set environment variables 95 | --------------------------------- 96 | Many applications use `environment variables`_ for configuration, and 97 | Flask-Dance is no exception. You'll need to set the following environment 98 | variables: 99 | 100 | * ``FLASK_APP``: set this to ``github.py`` 101 | * ``GITHUB_OAUTH_CLIENT_ID``: set this to the client ID you got 102 | from GitHub. 103 | * ``GITHUB_OAUTH_CLIENT_SECRET``: set this to the client secret 104 | you got from GitHub. 105 | * ``OAUTHLIB_INSECURE_TRANSPORT``: set this to ``true``. This indicates that 106 | you're doing local testing, and it's OK to use HTTP instead of HTTPS for 107 | OAuth. You should only do this for local testing. 108 | Do **not** set this in production! [`oauthlib docs`_] 109 | 110 | The easiest way to set these environment variables is to define them in 111 | an ``.env`` file. You can then install the `python-dotenv`_ package 112 | to make Flask automatically read this file when you run the dev server. 113 | This repository has a ``.env.example`` file that you can copy to 114 | ``.env`` to get a head start. 115 | 116 | Step 4: Run your app and login with GitHub! 117 | ------------------------------------------- 118 | Run your app using the ``flask`` command:: 119 | 120 | flask run 121 | 122 | Then, go to http://localhost:5000/ to visit your app and log in with GitHub! 123 | 124 | If you get an error message that says "Could not locate a Flask application", 125 | then you need to install the `python-dotenv`_ package using ``pip``:: 126 | 127 | pip install python-dotenv 128 | 129 | Once the package is installed, try the ``flask run`` command again! 130 | 131 | Learn more! 132 | ``````````` 133 | `Fork this GitHub repo`_ so that you can make changes to it. Read the 134 | documentation for `Flask`_ and `Flask-Dance`_ to learn what's possible. 135 | Ask questions, learn as you go, build your own OAuth-enabled web application, 136 | and don't forget to be awesome! 137 | 138 | 139 | .. _Flask: http://flask.pocoo.org/docs/ 140 | .. _Flask-Dance: http://flask-dance.readthedocs.org/ 141 | .. _GitHub: https://github.com/ 142 | .. _Heroku: https://www.heroku.com/ 143 | .. _environment variables: https://en.wikipedia.org/wiki/Environment_variable 144 | .. _python-dotenv: https://github.com/theskumar/python-dotenv 145 | .. _oauthlib docs: http://oauthlib.readthedocs.org/en/latest/oauth2/security.html#envvar-OAUTHLIB_INSECURE_TRANSPORT 146 | .. _virtual environment: https://docs.python.org/3.7/library/venv.html 147 | .. _Fork this GitHub repo: https://help.github.com/articles/fork-a-repo/ 148 | 149 | .. |heroku-deploy| image:: https://www.herokucdn.com/deploy/button.png 150 | :target: https://heroku.com/deploy 151 | :alt: Deploy to Heroku 152 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Flask-Dance with GitHub", 3 | "description": "Testing Flask-Dance's OAuth integration with GitHub", 4 | "keywords": [ 5 | "oauth" 6 | ], 7 | "website": "https://github.com/singingwolfboy/flask-dance-github", 8 | "repository": "https://github.com/singingwolfboy/flask-dance-github", 9 | "env": { 10 | "FLASK_SECRET_KEY": { 11 | "description": "A secret key for verifying the integrity of signed cookies.", 12 | "generator": "secret" 13 | }, 14 | "GITHUB_OAUTH_CLIENT_ID": { 15 | "description": "The OAuth client ID for your application, assigned by GitHub", 16 | "value": "client-id-goes-here" 17 | }, 18 | "GITHUB_OAUTH_CLIENT_SECRET": { 19 | "description": "The OAuth client secret for your application, assigned by GitHub", 20 | "value": "client-secret-goes-here" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | betamax 3 | pathlib2; python_version < '3.0' 4 | -------------------------------------------------------------------------------- /github.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, redirect, url_for 3 | from flask_dance.contrib.github import make_github_blueprint, github 4 | 5 | app = Flask(__name__) 6 | app.secret_key = os.environ.get("FLASK_SECRET_KEY", "supersekrit") 7 | app.config["GITHUB_OAUTH_CLIENT_ID"] = os.environ.get("GITHUB_OAUTH_CLIENT_ID") 8 | app.config["GITHUB_OAUTH_CLIENT_SECRET"] = os.environ.get("GITHUB_OAUTH_CLIENT_SECRET") 9 | github_bp = make_github_blueprint() 10 | app.register_blueprint(github_bp, url_prefix="/login") 11 | 12 | 13 | @app.route("/") 14 | def index(): 15 | if not github.authorized: 16 | return redirect(url_for("github.login")) 17 | resp = github.get("/user") 18 | assert resp.ok 19 | return "You are @{login} on GitHub".format(login=resp.json()["login"]) 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask-dance 2 | gunicorn 3 | -------------------------------------------------------------------------------- /tests/README.rst: -------------------------------------------------------------------------------- 1 | Automated Tests 2 | =============== 3 | 4 | To run the tests, you need to install two Python packages: 5 | Pytest_ and Betamax_. You can install them with ``pip``, like this: 6 | 7 | .. code-block:: bash 8 | 9 | pip install pytest betamax 10 | 11 | Then you can run the tests using the ``pytest`` command: 12 | 13 | .. code-block:: bash 14 | 15 | pytest 16 | 17 | Fixtures 18 | -------- 19 | 20 | The ``conftest.py`` file contains the `Pytest fixtures`_ that 21 | the tests use. We want to use the ``betamax_record_flask_dance`` 22 | fixture `provided by Flask-Dance 23 | `_, 24 | so we define an ``app`` fixture and a ``flask_dance_sessions`` 25 | fixture. We also define a ``github_authorized`` fixture, 26 | which will use a ``MemoryStorage`` object to tell Flask-Dance 27 | that the user is already authorized with GitHub. 28 | 29 | Cassettes 30 | --------- 31 | 32 | The ``cassettes`` directory contains recorded HTTP sessions, 33 | which the automated tests can replay. To record new HTTP sessions, 34 | delete the files in that directory, and then run the tests again. 35 | 36 | When making live HTTP requests to GitHub, such as when you record 37 | new HTTP sessions, you must have a valid OAuth token for GitHub. 38 | Put this OAuth token in the ``GITHUB_OAUTH_ACCESS_TOKEN`` environment 39 | variable, and the automated tests will automatically pick it up. 40 | 41 | .. _Pytest: https://pytest.org/ 42 | .. _Betamax: https://betamax.readthedocs.io/ 43 | .. _Pytest fixtures: https://docs.pytest.org/en/latest/fixture.html 44 | -------------------------------------------------------------------------------- /tests/cassettes/test_index_authorized.json: -------------------------------------------------------------------------------- 1 | {"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"} -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | try: 4 | from pathlib import Path 5 | except ImportError: 6 | from pathlib2 import Path 7 | 8 | import pytest 9 | from betamax import Betamax 10 | from flask_dance.consumer.storage import MemoryStorage 11 | from flask_dance.contrib.github import github 12 | 13 | toplevel = Path(__file__).parent.parent 14 | sys.path.insert(0, str(toplevel)) 15 | from github import app as flask_app, github_bp 16 | 17 | 18 | GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_OAUTH_ACCESS_TOKEN", "fake-token") 19 | 20 | with Betamax.configure() as config: 21 | config.cassette_library_dir = str(toplevel / "tests" / "cassettes") 22 | config.define_cassette_placeholder("", GITHUB_ACCESS_TOKEN) 23 | 24 | 25 | @pytest.fixture 26 | def github_authorized(monkeypatch): 27 | """ 28 | Monkeypatch the GitHub Flask-Dance blueprint so that the 29 | OAuth token is always set. 30 | """ 31 | storage = MemoryStorage({"access_token": GITHUB_ACCESS_TOKEN}) 32 | monkeypatch.setattr(github_bp, "storage", storage) 33 | return storage 34 | 35 | 36 | @pytest.fixture 37 | def app(): 38 | return flask_app 39 | 40 | 41 | @pytest.fixture 42 | def flask_dance_sessions(): 43 | """ 44 | Necessary to use the ``betamax_record_flask_dance`` fixture 45 | from Flask-Dance 46 | """ 47 | return github 48 | -------------------------------------------------------------------------------- /tests/test_github.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_index_unauthorized(app): 5 | with app.test_client() as client: 6 | response = client.get("/", base_url="https://example.com") 7 | 8 | assert response.status_code == 302 9 | redirect = response.headers.get("Location", None) 10 | assert redirect == "https://example.com/login/github" 11 | 12 | 13 | @pytest.mark.usefixtures("github_authorized", "betamax_record_flask_dance") 14 | def test_index_authorized(app): 15 | with app.test_client() as client: 16 | response = client.get("/", base_url="https://example.com") 17 | 18 | assert response.status_code == 200 19 | text = response.get_data(as_text=True) 20 | assert text == "You are @singingwolfboy on GitHub" 21 | --------------------------------------------------------------------------------