I have successfully followed the examples in the documentation of Flask-Dance to add Oauth from GitHub, and manage the users with SQLAlchemy. However, I am unable to add more providers. I tried to simply add another blueprint for Twitter and registering it, but I get various errors when trying to login with Twitter. Also, I end up with lots of duplicated code, so this is not a good approach.
Is there a better way to add another provider?
import sys
from flask import Flask, redirect, url_for, flash, render_template
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound
from flask_dance.contrib.github import make_github_blueprint, github
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin, SQLAlchemyBackend
from flask_dance.consumer import oauth_authorized, oauth_error
from flask_login import (
LoginManager, UserMixin, current_user,
login_required, login_user, logout_user
)
# setup Flask application
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")
# setup database models
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///multi.db"
db = SQLAlchemy()
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(256), unique=True)
# ... other columns as needed
class OAuth(db.Model, OAuthConsumerMixin):
user_id = db.Column(db.Integer, db.ForeignKey(User.id))
user = db.relationship(User)
# setup login manager
login_manager = LoginManager()
login_manager.login_view = 'github.login'
#login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# setup SQLAlchemy backend
blueprint.backend = SQLAlchemyBackend(OAuth, db.session, user=current_user)
# create/login local user on successful OAuth login
#oauth_authorized.connect_via(blueprint)
def github_logged_in(blueprint, token):
if not token:
flash("Failed to log in with {name}".format(name=blueprint.name))
return
# figure out who the user is
resp = blueprint.session.get("/user")
if resp.ok:
username = resp.json()["login"]
query = User.query.filter_by(username=username)
try:
user = query.one()
except NoResultFound:
# create a user
user = User(username=username)
db.session.add(user)
db.session.commit()
login_user(user)
flash("Successfully signed in with GitHub")
else:
msg = "Failed to fetch user info from {name}".format(name=blueprint.name)
flash(msg, category="error")
# notify on OAuth provider error
#oauth_error.connect_via(blueprint)
def github_error(blueprint, error, error_description=None, error_uri=None):
msg = (
"OAuth error from {name}! "
"error={error} description={description} uri={uri}"
).format(
name=blueprint.name,
error=error,
description=error_description,
uri=error_uri,
)
flash(msg, category="error")
#app.route("/logout")
#login_required
def logout():
logout_user()
flash("You have logged out")
return redirect(url_for("index"))
#app.route("/")
def index():
return render_template("home.html")
# hook up extensions to app
db.init_app(app)
login_manager.init_app(app)
if __name__ == "__main__":
if "--setup" in sys.argv:
with app.app_context():
db.create_all()
db.session.commit()
print("Database tables created")
else:
app.run(debug=True)
Here is a sample project that uses multiple providers simultaneously: flask-dance-multi-provider. You can read through the code to see how it works. Does that help?
Related
I used the same code for google oauth and I used flask-dance. This worked with google, but didn't work with discord. I get an error message saying that I need to be logged in to reach the home page, meaning that the discord account hasn't been registered in the sqlite database.
Here is my code
from flask import Flask, render_template, redirect, url_for, flash, Blueprint
from flask_login import current_user, login_user, login_required
from flask_dance.contrib.google import make_google_blueprint, google
from flask_dance.contrib.discord import make_discord_blueprint, discord
from flask_dance.consumer import oauth_authorized, oauth_error
from flask_dance.consumer.storage.sqla import SQLAlchemyStorage
from sqlalchemy.orm.exc import NoResultFound
from __init__ import db
from models import User, OAuth
discord_blueprint = make_discord_blueprint(client_id= "1001891783061553233", client_secret="QMvsUwbFGCLgYWgr8GAQ5ae2WibPTeDB", scope=["identify", "email"])
discord_bp = make_discord_blueprint(storage = SQLAlchemyStorage(OAuth, db.session, user = current_user))
#oauth_authorized.connect_via(discord_blueprint)
def discord_logged_in(blueprint, token):
if not token:
flash("Failed to log in", category="error")
return
resp = blueprint.session.get("/users/#me")
if not resp:
msg = "Failed to fetch user info"
flash(msg, category="error")
return
discord_name = resp.json()["name"]
discord_user_id = resp.json() ["id"]
query = OAuth.query.filter_by(
provider = blueprint.name, provider_user_id = discord_user_id
)
try:
oauth = query.one()
except(NoResultFound):
discord_user_login = discord_name
oauth = OAuth(provider = blueprint.name,
provider_user_id = discord_user_id,
provider_user_login = discord_user_login,
token=token,
)
if current_user.is_anonymous:
if oauth.user:
login_user(oauth.user)
else:
user = User(username = discord_name)
oauth.user = user
db.session.add_all([user, oauth])
db.session.commit()
login_user(user)
else:
if oauth.user:
if current_user != oauth.user:
url = url_for("auth.merge", username = oauth.user.username)
return redirect(url)
else:
oauth.user = current_user
db.session.add(oauth)
db.commit()
return redirect(url_for("main.profile"))
The google code is the same as the discord one and my redirect uri is localhost:5000/login/"oauthprovider"/authorized. For some reason the discord user isnt registered in the database?
I took me many hours to figure it out because there's literally 0 guides for this, but this the approach that worked for me in the end. This is an extract from my source, so you can see the full implementation here. I'm still working out some kinks like trying to be able to turn off os.environ['OAUTHLIB_INSECURE_TRANSPORT'] but I can login at least.
#REST_API.route('/register', methods=['GET', 'POST'])
def register():
discord_data = None
if discord.authorized:
discord_info_endpoint = '/api/users/#me'
try:
discord_data = discord.get(discord_info_endpoint).json()
except oauthlib.oauth2.rfc6749.errors.TokenExpiredError:
pass
#REST_API.route('/discord')
def discord_login():
return redirect(url_for('discord.login'))
if __name__ == "__main__":
discord_client_id = os.getenv("DISCORD_CLIENT_ID")
discord_client_secret = os.getenv("DISCORD_CLIENT_SECRET")
REST_API.secret_key = os.getenv("secret_key")
discord_blueprint = make_discord_blueprint(
client_id = discord_client_id,
client_secret = discord_client_secret,
scope = ["identify"],
)
REST_API.register_blueprint(discord_blueprint,url_prefix="/discord")
I am trying to create a flask app that will allow users to login, thereafter, they will be redirected to a specific dashboard created and being served with Bokeh.
So, for example, we have an user1, at the beginning he will start in https:myurl/login, after successful login he will be redirected to https:myurl/user1 where his specific dashboard is.
So, my question is, how I can avoid user1 accessing dashboard from other users user2, user3, etc. It is actually possible to do that? I am relatively new to flask, so my apologies if the question sounds silly.
from multiprocessing import connection
from functools import wraps
from re import A
from flask import Flask, render_template, request, flash, redirect, url_for, session
import sqlite3
from sqlalchemy import DATE
# Setup
app = Flask(__name__)
app.secret_key = "my_key"
# Routes
#app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
connection = sqlite3.connect("user_login.db")
cursor = connection.cursor()
# Get what the user has typed in the HTML form
username = request.form["username"]
password = request.form["password"]
# SQL query
cursor.execute(
"SELECT * FROM users WHERE username=? AND password=?", (username, password)
)
data = cursor.fetchone()
if data:
session["username"] = data[1]
session["password"] = data[2]
return redirect(url_for("user({data[1]})"))
# return redirect(f"https://myurl/{session['username']}", code=302)
else:
flash("Username and Password Mismatch", "DANGER! Please try again")
# Render HTML template
return render_template("login.html")
# Check if user is logged in
# def is_logged_in(f):
# #wraps(f)
# def secure_function(*args, **kwargs):
# if "logged_in" in session:
# return f(*args, **kwargs)
# else:
# flash("Unauthorized, please login", "danger")
# return redirect(url_for("login"))
# return secure_function
#app.route("/<username>")
def user(username):
if username == session['username']:
return redirect(
f"https://myurl/{session['username']}", code=302
)
else:
return flash("Unauthorized")
# #app.route('/')
# def logged():
# return redirect(f"https://myurl/{session['username']}", code=302)
if __name__ == "__main__":
app.run(debug=True)
How about verifying if the
current_user.username == myurl/<username>
.username being the name of your user in your Models(if it is name then current_user.name, etc.)
Like
#app.route("/dashboard/<username>")
def dashboard(username):
if username == current_user.username:
#proceed
else:
return "Access Denied"
*** Edit ***
Your provided code for the return statement
redirect(url_for("user({data[1]})"))
Could be written as:
return redirect(url_for('user', username = data[1]))
I need to ensure transactionality with flask requests. Is there a way to store a database session per request and use that one?
The easiest way is to create a simple extension for session initialization and commit changes after successful request. Here is an example:
from datetime import datetime
from flask import Flask, current_app, _app_ctx_stack
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base, DeferredReflection
from sqlalchemy import Column, String, Integer
# Base model without connection(see: DBExtension.connect()) and a few demo models
Base = declarative_base(cls=DeferredReflection)
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String())
class Log(Base):
__tablename__ = 'log'
id = Column(Integer, primary_key=True, autoincrement=True)
text = Column(String())
# extension for getting db session
class DBExtension:
def __init__(self, app: Flask = None):
self.app = app
if app is not None:
self.init_app(app)
def init_app(self, app):
app.teardown_appcontext(self.teardown)
def connect(self):
# init engine and set into metadata
engine = create_engine(current_app.config['DB_CONNECTION'], echo=True)
Session = sessionmaker(bind=engine)
session = Session()
# create_all - just for demo(init structure...)
Base.metadata.create_all(engine)
Base.metadata.bind = engine
Base.prepare(engine)
return session
def teardown(self, exception):
ctx = _app_ctx_stack.top
if hasattr(ctx, 'session'):
ctx.session.close()
#property
def session(self):
# connect or get active connection
ctx = _app_ctx_stack.top
if ctx is not None:
if not hasattr(ctx, 'session'):
ctx.session = self.connect()
return ctx.session
app = Flask(__name__)
app.config.update({'DB_CONNECTION': 'sqlite:///test.db'}) # sqlite for demo
db = DBExtension(app)
#app.teardown_request
def teardown_request_func(error=None):
# request processing completed(before requests functions, endpoints and other logic)
if not error:
# one more record and commit all changes in one transaction
db.session.add(Log(text='{dt}. teardown_request'.format(dt=datetime.utcnow())))
try:
db.session.commit()
except Exception as e:
print(e)
# do something
#app.before_request
def before_request_func():
# add a new record before request
db.session.add(Log(text='{dt}. before request'.format(dt=datetime.utcnow())))
#app.route('/user/<name>')
def user_route(name: str):
# do something with db session
db.session.add(Log(text='saving user {user}'.format(user=name)))
user = User(name=name)
db.session.add(user)
# session.add(newModel) or session.delete(oneMoreModel) etc.
return 'done' # return without commit. see: teardown_request_func
if __name__ == '__main__':
app.run(host='localhost', port=5000, debug=True)
Run server, open http://localhost:5000/user/DavidBoshton and see logs(BEGIN-...-COMMIT).
I've been stuck on this problem for a couple hours. I'm new to django and automated testing, and I've been playing around with Selenium and django's StaticLiveServerTestCase. I've been trying to test my login form (which works fine when I use runserver and test it myself, by the way.)
Everything is working great, except I can't seem to successfully login my test user. I've narrowed down the break point to django's User.authenticate method.
The User object is created successfully in my setUp and I can confirm that by accessing it's attributes in my test method. However, authenticate fails.
I've looked at the following for help but they didn't get me very far:
Django Unittests Client Login: fails in test suite, but not in Shell
Authentication failed in Django test
Any idea why authenticate fails? Do I need to add something to my settings?
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
class AccountTestCase(StaticLiveServerTestCase):
def setUp(self):
self.selenium = webdriver.Chrome()
super().setUp()
User.objects.create(username='test', email='test#test.com', password='Test1234', is_active=True)
def tearDownClass(self):
self.selenium.quit()
super().tearDown()
def test_register(self):
user = authenticate(username='test', password='Test1234')
if user is not None: # prints Backend login failed
print("Backend login successful")
else:
print("Backend login failed")
user = User.objects.get(username='test')
print(user)
print(user.username) # prints test
print(user.password) # prints Test1234
I found the issue. The User.authenticate() method hashes the password provided. However, I set the password directly when creating the user, which means it was stored as Test1234, so the hashed password provided during authentication did not match 'Test1234', hence the failure.
To properly store a hashed password, you need to use the set_password() method.
Updated setUp code:
def setUp(self):
self.selenium = webdriver.Chrome()
super().setUp()
user = User.objects.create(username='test', email='test#test.com', is_active=True)
user.set_password('Test1234')
user.save()
Using create_superuser resolved the issue. Below code resolves it.
from django.contrib.auth.models import User
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.test import Client
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver
import time
from django.contrib.auth import authenticate
#from apps.digital.models import User
class MyTests(StaticLiveServerTestCase):
port = 0
host = 'my host'
def setUp(self):
super(MyTests, self).setUp()
self.selenium = WebDriver()
self.client = Client()
self.user = User.objects.create_superuser(username='test', password='Test1234', email='test#test.com', is_active=True)
self.user.save()
def tearDown(self):
self.selenium.quit()
super(MyTests, self).tearDown()
def test_login(self):
self.user = authenticate(username='test', password='Test1234')
if self.user is not None: # prints Backend login failed
self.user = User.objects.get(username='test')
print(self.user.username) # prints test
print(self.user.password) # prints Test1234
self.login = self.client.login(username='test', password='Test1234')
self.assertEqual(self.login, True)
print("Backend login successful")
self.selenium.get('%s%s' % (self.live_server_url, '/admin/'))
username_input = self.selenium.find_element_by_name("username")
username_input.send_keys(self.user.username)
password_input = self.selenium.find_element_by_name("password")
password_input.send_keys('Test1234')
self.selenium.find_element_by_xpath('//input[#value="Log in"]').click()
time.sleep(1)
else:
print("Backend login failed")
User Authentication is getting passed but when I try to login with the same credentials, the login fails.
from django.contrib.auth.models import User
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.test import Client
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver
import time
from django.contrib.auth import authenticate
#from apps.digital.models import User
class MyTests(StaticLiveServerTestCase):
port = 0
host = '<my host>'
def setUp(self):
super(MyTests, self).setUp()
self.selenium = WebDriver()
self.client = Client()
self.user = User.objects.create(username='test', email='test#test.com', is_active=True)
self.user.set_password('Test1234')
self.user.save()
def tearDown(self):
self.selenium.quit()
super(MyTests, self).tearDown()
def test_login(self):
self.user = authenticate(username='test', password='Test1234')
if self.user is not None: # prints Backend login failed
self.user = User.objects.get(username='test')
print(self.user.username) # prints test
print(self.user.password) # prints Test1234
self.login = self.client.login(username='test', password='Test1234')
self.assertEqual(self.login, True)
print("Backend login successful")
self.selenium.get('%s%s' % (self.live_server_url, '/admin/'))
username_input = self.selenium.find_element_by_name("username")
username_input.send_keys(self.user.username)
password_input = self.selenium.find_element_by_name("password")
password_input.send_keys('Test1234')
self.selenium.find_element_by_xpath('//input[#value="Log in"]').click()
time.sleep(1)
else:
print("Backend login failed")
The following test code does not pass even though manually submitting the form on my web interface actually does work.
import os
from flask.ext.testing import TestCase
from flask import url_for
from config import _basedir
from app import app, db
from app.users.models import User
class TestUser(TestCase):
def create_app(self):
"""
Required method. Always implement this so that app is returned with context.
"""
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(_basedir, 'test.db')
app.config['WTF_CSRF_ENABLED'] = False # This must be disabled for post to succeed during tests
self.client = app.test_client()
ctx = app.app_context()
ctx.push()
return app
def setUp(self):
db.create_all()
#pass
#app.teardown_appcontext
def tearDown(self):
db.session.remove()
db.drop_all()
#pass
def test_admin_home(self):
# url is the admin home page
url = url_for('admin.index')
resp = self.client.get(url)
self.assertTrue(resp.status_code == 200)
def test_admin_registration(self):
url = url_for('admin.register_view')
data = {'username': 'admin', 'email': 'admin#example.com', 'password': 'admin'}
resp = self.client.post(url, data)
self.assertTrue(resp.status_code == 200)
u = User.query.filter_by(username=u'admin').first()
self.assertTrue(u.username == 'admin') # <----- This fails. Why?
After the test client has post to the register_view url and returns a 200 OK response, I fail to retrieve the 'admin' user from the test database. Why is this so?
Here's the view code (this is a flask admin view)
from flask import request
from flask.ext.admin import expose, AdminIndexView, helpers
from app.auth.forms import LoginForm, RegistrationForm
from app.users.models import User
from app import db
class MyAdminIndexView(AdminIndexView):
#expose('/', methods=('GET', 'POST'))
def index(self):
# handle user login
form = LoginForm(request.form)
self._template_args['form'] = form
return super(MyAdminIndexView, self).index()
#expose('/register/', methods=('GET', 'POST'))
def register_view(self):
# handle user registration
form = RegistrationForm(request.form)
if helpers.validate_form_on_submit(form):
user = User()
form.populate_obj(user)
db.session.add(user)
db.session.commit()
self._template_args['form'] = form
return super(MyAdminIndexView, self).index()
Dumbest mistake ever.
The offending line in my test code is
resp = self.client.post(url, data)
It should be
resp = self.client.post(url, data=data)
I managed to track it down by painstakingly walking through the logic and inserting ipdb.set_trace() step by step until I found the bad POST request made by my client.