├── README.md ├── .env.template ├── requirements.txt ├── .gitignore ├── LICENSE ├── templates └── index.html └── app.py /README.md: -------------------------------------------------------------------------------- 1 | flask-oauth-example 2 | =================== 3 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | GITHUB_CLIENT_ID= 2 | GITHUB_CLIENT_SECRET= 3 | GOOGLE_CLIENT_ID= 4 | GOOGLE_CLIENT_SECRET= 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.6.2 2 | certifi==2023.5.7 3 | charset-normalizer==3.1.0 4 | click==8.1.3 5 | Flask==2.3.2 6 | Flask-Login==0.6.2 7 | Flask-SQLAlchemy==3.0.3 8 | greenlet==2.0.2 9 | idna==3.4 10 | itsdangerous==2.1.2 11 | Jinja2==3.1.2 12 | MarkupSafe==2.1.2 13 | python-dotenv==1.0.0 14 | requests==2.31.0 15 | SQLAlchemy==2.0.15 16 | typing_extensions==4.6.2 17 | urllib3==2.0.2 18 | Werkzeug==2.3.4 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # application files 57 | venv* 58 | *.sqlite 59 | .* 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Miguel Grinberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Flask + OAuth 2.0 Demo 4 | 5 | 6 | 7 | 12 |
13 | {% with messages = get_flashed_messages() %} 14 | {% if messages %} 15 | 20 | {% endif %} 21 | {% endwith %} 22 | {% if current_user.is_authenticated %} 23 |

Hi, {{ current_user.username }}!

24 |

25 | Logout 26 |

27 | {% else %} 28 |

29 | Login with Google 30 | Login with GitHub 31 |

32 | {% endif %} 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import secrets 3 | from urllib.parse import urlencode 4 | 5 | from dotenv import load_dotenv 6 | from flask import Flask, redirect, url_for, render_template, flash, session, \ 7 | current_app, request, abort 8 | from flask_sqlalchemy import SQLAlchemy 9 | from flask_login import LoginManager, UserMixin, login_user, logout_user,\ 10 | current_user 11 | import requests 12 | 13 | load_dotenv() 14 | 15 | app = Flask(__name__) 16 | app.config['SECRET_KEY'] = 'top secret!' 17 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite' 18 | app.config['OAUTH2_PROVIDERS'] = { 19 | # Google OAuth 2.0 documentation: 20 | # https://developers.google.com/identity/protocols/oauth2/web-server#httprest 21 | 'google': { 22 | 'client_id': os.environ.get('GOOGLE_CLIENT_ID'), 23 | 'client_secret': os.environ.get('GOOGLE_CLIENT_SECRET'), 24 | 'authorize_url': 'https://accounts.google.com/o/oauth2/auth', 25 | 'token_url': 'https://accounts.google.com/o/oauth2/token', 26 | 'userinfo': { 27 | 'url': 'https://www.googleapis.com/oauth2/v3/userinfo', 28 | 'email': lambda json: json['email'], 29 | }, 30 | 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], 31 | }, 32 | 33 | # GitHub OAuth 2.0 documentation: 34 | # https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps 35 | 'github': { 36 | 'client_id': os.environ.get('GITHUB_CLIENT_ID'), 37 | 'client_secret': os.environ.get('GITHUB_CLIENT_SECRET'), 38 | 'authorize_url': 'https://github.com/login/oauth/authorize', 39 | 'token_url': 'https://github.com/login/oauth/access_token', 40 | 'userinfo': { 41 | 'url': 'https://api.github.com/user/emails', 42 | 'email': lambda json: json[0]['email'], 43 | }, 44 | 'scopes': ['user:email'], 45 | }, 46 | } 47 | 48 | db = SQLAlchemy(app) 49 | login = LoginManager(app) 50 | login.login_view = 'index' 51 | 52 | 53 | class User(UserMixin, db.Model): 54 | __tablename__ = 'users' 55 | id = db.Column(db.Integer, primary_key=True) 56 | username = db.Column(db.String(64), nullable=False) 57 | email = db.Column(db.String(64), nullable=True) 58 | 59 | 60 | @login.user_loader 61 | def load_user(id): 62 | return db.session.get(User, int(id)) 63 | 64 | 65 | @app.route('/') 66 | def index(): 67 | return render_template('index.html') 68 | 69 | 70 | @app.route('/logout') 71 | def logout(): 72 | logout_user() 73 | flash('You have been logged out.') 74 | return redirect(url_for('index')) 75 | 76 | 77 | @app.route('/authorize/') 78 | def oauth2_authorize(provider): 79 | if not current_user.is_anonymous: 80 | return redirect(url_for('index')) 81 | 82 | provider_data = current_app.config['OAUTH2_PROVIDERS'].get(provider) 83 | if provider_data is None: 84 | abort(404) 85 | 86 | # generate a random string for the state parameter 87 | session['oauth2_state'] = secrets.token_urlsafe(16) 88 | 89 | # create a query string with all the OAuth2 parameters 90 | qs = urlencode({ 91 | 'client_id': provider_data['client_id'], 92 | 'redirect_uri': url_for('oauth2_callback', provider=provider, 93 | _external=True), 94 | 'response_type': 'code', 95 | 'scope': ' '.join(provider_data['scopes']), 96 | 'state': session['oauth2_state'], 97 | }) 98 | 99 | # redirect the user to the OAuth2 provider authorization URL 100 | return redirect(provider_data['authorize_url'] + '?' + qs) 101 | 102 | 103 | @app.route('/callback/') 104 | def oauth2_callback(provider): 105 | if not current_user.is_anonymous: 106 | return redirect(url_for('index')) 107 | 108 | provider_data = current_app.config['OAUTH2_PROVIDERS'].get(provider) 109 | if provider_data is None: 110 | abort(404) 111 | 112 | # if there was an authentication error, flash the error messages and exit 113 | if 'error' in request.args: 114 | for k, v in request.args.items(): 115 | if k.startswith('error'): 116 | flash(f'{k}: {v}') 117 | return redirect(url_for('index')) 118 | 119 | # make sure that the state parameter matches the one we created in the 120 | # authorization request 121 | if request.args['state'] != session.get('oauth2_state'): 122 | abort(401) 123 | 124 | # make sure that the authorization code is present 125 | if 'code' not in request.args: 126 | abort(401) 127 | 128 | # exchange the authorization code for an access token 129 | response = requests.post(provider_data['token_url'], data={ 130 | 'client_id': provider_data['client_id'], 131 | 'client_secret': provider_data['client_secret'], 132 | 'code': request.args['code'], 133 | 'grant_type': 'authorization_code', 134 | 'redirect_uri': url_for('oauth2_callback', provider=provider, 135 | _external=True), 136 | }, headers={'Accept': 'application/json'}) 137 | if response.status_code != 200: 138 | abort(401) 139 | oauth2_token = response.json().get('access_token') 140 | if not oauth2_token: 141 | abort(401) 142 | 143 | # use the access token to get the user's email address 144 | response = requests.get(provider_data['userinfo']['url'], headers={ 145 | 'Authorization': 'Bearer ' + oauth2_token, 146 | 'Accept': 'application/json', 147 | }) 148 | if response.status_code != 200: 149 | abort(401) 150 | email = provider_data['userinfo']['email'](response.json()) 151 | 152 | # find or create the user in the database 153 | user = db.session.scalar(db.select(User).where(User.email == email)) 154 | if user is None: 155 | user = User(email=email, username=email.split('@')[0]) 156 | db.session.add(user) 157 | db.session.commit() 158 | 159 | # log the user in 160 | login_user(user) 161 | return redirect(url_for('index')) 162 | 163 | 164 | with app.app_context(): 165 | db.create_all() 166 | 167 | if __name__ == '__main__': 168 | app.run(debug=True) 169 | --------------------------------------------------------------------------------