In short:
By only using the Flask micro-framework (and its dependencies) can we perform an internal redirect from one route to another?
For example:
User submits the registration form (both username and password) to #app.route('/register', methods=['POST'])
If the registration is successful, Flask internally does an HTTP POST to #app.route('/login', methods['POST']) passing the username and password
Process and log in the user
Details:
I am building a REST API using Flask and the Flask-JWT extension. More specifically I'm implementing the login and registration.
Login works perfectly and returns a JSON object with a token.
Following is my (login) authentication handler (i.e. /auth (POST request) - Default Flask-JWT authentication URL rule):
#jwt.authentication_handler
def authenticate(username, password):
user = User.query.filter_by(username=username).first()
if user and user.verify_password(password):
return user
return None
A successful login returns:
{
"token": "<jwt-token>"
}
Following is my registration route:
#app.route('/register', methods=['PUT'])
def register():
username = request.form.get('username')
password = request.form.get('password')
if username is None or password is None:
abort(400) # missing parameters
user = User.query.filter_by(username=username).first()
if user:
abort(400) # user exists
else:
user = User(user=user)
user.hash_password(password)
db.session.add(user)
db.session.commit()
# How do we generate a token?
# Perform an internal redirect to the login route?
return jsonify({'token': <jwt-token>}), 201
You should use the Post-Redirect-Get pattern.
from flask import Flask, redirect, request, render_template
app = Flask("the_flask_module")
#app.route('/', methods=["GET", "POST"])
def post_redirect_get():
if request.method == "GET":
return render_template("post_redirect_get.html")
else:
# Use said data.
return redirect("target", code=303)
#app.route("/target")
def target():
return "I'm the redirected function"
app.run(host="0.0.0.0", port=5001)
And if you want to pass data to the target function (like that token) you can use the session object to store it
So that would break down something like
#app.route('/register', methods=['PUT'])
def register():
username = request.form.get('username')
password = request.form.get('password')
if username is None or password is None:
abort(400) # missing parameters
user = User.query.filter_by(username=username).first()
if user:
abort(400) # user exists
else:
user = User(user=user)
user.hash_password(password)
db.session.add(user)
db.session.commit()
# How do we generate a token?
redirect("login_success", code=307)
#app.route("login_success", methods=["GET", "POST"])
#jwt_required()
def login_success():
return "Redirected Success!"
Edit:
I haven't used Flask-JWT before and didn't know about the post requirement. But you can tell Flask to redirect with the current method used (rather than a get request) by passing the redirect function code=307.. Hopefully that solves your extended problem.
Related
I have setup a loopback API, and I plan to use the login as such flask would make requests to loopback and loopback returns an accessToken
For example login to dashboard:
# Login route
#app.route("/login", methods=['GET', 'POST'])
def login():
status = ""
url_login = 'http://localhost:3000/api/Users/login'
try:
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
payload_login = {
"username": str(username),
"password":str(password)
}
print(payload_login)
r = requests.post(url_login, data=payload_login).text
access_token = json.loads(r)
# access_token = r['id']
# access_token = json.loads(access_token)
print("Access Token: " + str(access_token['id']))
return redirect('/') #CHANGE TO 404 PAGE
except Exception as e:
print(e)
return redirect('/') #CHANGE TO 404 PAGE
return render_template('login.html')
#app.route('/dashboard', methods=['GET', 'POST'])
def logged_in_dashboard():
return render_template('index.html')
How do I set it up so that login to dashboard requires accessToken from loopback? In the past I've used app.config['ACCESS_KEY'] ='key' and have set that if it contains a token it would allow the user to login.
But I'm not sure if this is a good practice. Anything you would like to recommend that could handle lots of user logins?
Don't create requests to the API from within the API. To share functionality between endpoints, use functions. You need at least two functions here:
a function returns a token for valid credentials
a function that requires the token being present in the session or in the request Authorization header, for example
Check the approach that chans linked to for more implementation details: How do you implement token authentication in Flask?
Or the official tutorial for how to implement sessions: https://flask.palletsprojects.com/en/1.1.x/quickstart/#sessions
Which has something like this:
#app.route('/')
def index():
# this if is the login requirement
if 'username' in session:
return 'Logged in as %s' % escape(session['username'])
return 'You are not logged in'
#app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# Add logic for validating username and password here.
# If credentials are ok, set username to session.
session['username'] = request.form['username']
return redirect(url_for('index'))
return '''
<form method="post">
<p><input type=text name=username>
<p><input type=submit value=Login>
</form>
'''
I'm currently testing my Flask application using Pytest and ran into a problem with a POST request and a redirect. Let me explain a bit more.
A user wants to register for our new site, but must confirm they have an account with a different site. Once they confirm the credentials of the other account, they are taken to the register page. They can only hit the register page if coming from the confirmation page else they are redirected back to the home page.
I want to test this functionality and can successfully make a POST request to the confirmation page. If I don't specify follow_redirects=True and print the response data, I get the following HTML:
Redirecting...
Redirecting...
You should be redirected automatically to target URL: /register?emp_id=1. If not click the link.
Great! Exactly what I'm looking for! I want to be redirected to the registration page.
Now when I do specify follow_redirects=True and print out the response data, I expected the register page HTML to return. The response data instead returns the home page HTML.
I further investigated where the problem was. As I mentioned before, the only way you can hit the registration page is from the confirmation page. I took a look at the request.referrer attribute in the view during the test and it will return None. I attempted setting the Referrer header content in the test's POST request, but to no luck.
Here is the code I'm working with:
views.py
#app.route('/confirm', methods=['GET', 'POST'])
def confirm_bpm():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = BPMLoginForm()
if form.validate_on_submit():
bpm_user = BPMUser.query\
.filter(and_(BPMUser.group_name == form.group_name.data,
BPMUser.user_name == form.username.data,
BPMUser.password == encrypt(form.password.data)))\
.first()
if not bpm_user:
flash('BPM user account does not exist!')
url = url_for('confirm_bpm')
return redirect(url)
if bpm_user.user_level != 3:
flash('Only L3 Users can register through this portal.')
url = url_for('confirm_bpm')
return redirect(url)
url = url_for('register', emp_id=bpm_user.emp_id)
return redirect(url)
return render_template('login/confirm_bpm.html', form=form)
#app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
if request.method == 'GET' and\
request.referrer != request.url_root + 'confirm':
return redirect(url_for('index'))
emp_id = request.args.get('emp_id')
emp_id_exists = User.query.filter_by(emp_id=emp_id).first()
if emp_id_exists:
flash('User is already registered!')
return redirect(url_for('login'))
form = RegistrationForm()
if form.validate_on_submit():
new_user = User(login_type=form.login_type.data, login=form.login.data,
emp_id=emp_id)
new_user.set_password(form.password.data)
db.session.add(new_user)
db.session.commit()
flash('Registration successful!')
return redirect(url_for('login'))
return render_template('login/register.html', form=form)
TESTS
base.py
from config import TestConfig
from app import app, db
#pytest.fixture
def client():
"""
Initializes test requests for each individual test. The test request
keeps track of cookies.
"""
app.config.from_object(TestConfig)
client = app.test_client()
ctx = app.app_context()
ctx.push()
yield client
ctx.pop()
def confirm_bpm_login(client, group_name, username, password):
"""
POST to /confirm
"""
return client.post('/confirm', data=dict(
group_name=group_name,
username=username,
password=password,
submit=True
), follow_redirects=True)
test_auth.py
from app import db
from app.models import BPMCompany, BPMEmployee, User, BPMUser
from tests.base import client, db_data, login, confirm_bpm_login
def test_registration_page_from_confirm(client, db_data):
"""
Test registration page by HTTP GET request from "/confirm" url.
Should cause redirect to registration page.
!!! FAILING !!!
Reason: The POST to /confirm will redirect us to /register?emp_id=1,
but it will return the index.html because in the register view,
request.referrer does not recognize the POST is coming from /confirm_bpm
"""
bpm_user = BPMUser.query.filter_by(id=1).first()
rv = confirm_bpm_login(client, bpm_user.group_name,
bpm_user.user_name, 'jsmith01')
assert b'Register' in rv.data
The db_data parameter is a fixture in base.py that just populates the DB with the necessary data for the registration flow to work.
My goal is to test a complete registration flow without having to separate the confirmation and registration into two tests.
The Flask test client does not appear to add the Referer header when it follows redirects.
What you can do is implement your own version of following redirects. Maybe something like this:
def confirm_bpm_login(client, group_name, username, password):
"""
POST to /confirm
"""
response = client.post('/confirm', data=dict(
group_name=group_name,
username=username,
password=password,
submit=True
), follow_redirects=False)
if 300 <= response.status_code < 400:
response = client.get(response.headers['Location'], headers={
"Referer": 'http://localhost/confirm'
})
return response
Please test this, I wrote it from memory and may need some minor adjustments.
I am new to Flask and have been working on an existing app for its login module. It has to be delivered asap. The login credentials validates by Active Directory authentication, which is working as expected. I am referring this flask link which says
url_for('main', page=2): return the internal URL /?page=2. All optional keyword arguments are treated as GET parameters.
My index page is loaded from here (working perfectly):
#app.route("/", methods=['GET', 'POST'])
def login():
error = None
if request.method == 'POST':
userName = request.form['username']
pwd = request.form['password']
try:
ldapobject = auth.bind(userName,pwd)
return redirect(url_for('scheduler')) ----> NEED TO PASS USER AND PWD TO scheduler function using post here
except ldap.LDAPError, error_message:
error = 'Invalid Credentials. Please try again'
return render_template('login.html', error=error)
My scheduler function:
#app.route("/home", methods=['POST'])
def scheduler():
# some code here
# WANT TO HAVE USER CREDENTIALS IN THIS FUNCTION FROM login page
return render_template("scheduler.html", scheduler=form)
Problem: How to pass user's credentials to scheduler method as a POST call
If you want to persist user data between your application pages - you will need to store some session data on user side, e.g. cookies.
flask-login module (https://flask-login.readthedocs.org/en/) can do all the dirty work for you.
In short you need to create User class with several required fields and methods (https://flask-login.readthedocs.org/en/latest/#your-user-class) and specify several methods with decorators to load and check users that are being authenticated:
from flask_login import LoginManager
login_manager = LoginManager()
login_manager.init_app(app=app)
#login_manager.user_loader
def load_user(username):
return User(username)
#login_manager.request_loader
def load_from_request(request):
return User.auth_is_valid(request.authorization)
After that you can mark any routes that need user credentials with #login_requied decorator and access logged in user via current_user variable:
from flask_login import login_required, current_user
#app.route("/user_needed")
#login_required
def some_function():
print(current_user)
I am generating an auth token with the help of a separate serviceIn login from I am generating authentication token with help of some service. I generate the token in the login route. How can I prevent access to other routes until the login token is generated, and how can I access that token in the other routes?
#app.route('/login', methods=['GET', 'POST'])
def login():
error=None
if request.method=='POST':
if request.form['username']!='admin' or request.form['password']!='1234':
error ='Invalid Credentials. Please try again.'
else:
username=request.form['username']
password=request.form['password']
auth_url='http://192.168.206.133:5000/v2.0'
token = generateToken(username=username, password=password, auth_url=auth_url)
return redirect(url_for('getstats'))
return render_template('login.html', error=error)
# this route should require and use the auth token
#app.route('/metering')
def getstats():
return render_template('metering.html')
So it appears that you are trying to access the token in different routes.
I suggest storing them in a session. To do so, make sure to import the session variable from flask: from flask import session.
You want to set the token's value in the session. Right now, I will use auth_token as the session field, but you can use anything you want:
#app.route('/login', methods=['GET', 'POST'])
def login():
error=None
if request.method=='POST':
if request.form['username']!='admin' or request.form['password']!='1234':
error ='Invalid Credentials. Please try again.'
else:
username=request.form['username']
password=request.form['password']
auth_url='http://192.168.206.133:5000/v2.0'
token = generateToken(username=username, password=password, auth_url=auth_url)
session["auth_token"] = token # store the token in the session here
session["authenticated"] = True
return redirect(url_for('getstats'))
return render_template('login.html', error=error)
from functools import wraps
def authenticated_resource(function):
#wraps(function)
def decorated(*args, **kwargs):
if session.get("authenticated"):
return function(*args, **kwargs)
return redirect(url_for("login"))
return decorated
Then, to access the token:
#app.route('/metering')
#authenticated_resource
def getstats():
token = session.get("auth_token")
# you might want to verify that the token was in the session, as such
if token:
return render_template('metering.html')
else:
abort(403)
Note: to use abort, you also need to import that from flask.
I'm using flask. Here's my login function:
#app.route("/", methods=["GET", "POST"])
def login():
if request.method == "POST" and "imei" in request.form and "password" in request.form:
imei = request.form["imei"]
password = request.form["password"]
worked, name = checkDB(imei, password)
if worked:
uid = hashlib.sha256(imei.encode('utf-8')).hexdigest()
u = User(imei, name, password, uid)
USERS[uid] = u
login_user(u)
#return request.args.get("next")
return redirect(url_for("analyzer")) #THIS DOESENT WORK
else:
return redirect(url_for("login") + "?failure=true")
elif request.method == "GET" and request.args.get("failure"):
return render_template("auth.html", fail="true")
else:
return render_template("auth.html", fail="false")
When the line attempts to trigger (the one labeled THIS DOESENT WORK), it redirects to: /?next=%2Fwebike%2Fanalyzer.
The analyzer is simple:
#app.route('/analyzer', methods=['GET'])
#login_required
def analyzer():
return render_template('index.html')
What am I doing wrong? If the username and password are wrong, everything works as intended.
The login didn't work properly. You successfully redirect to /analyzer, but that redirects back to your login method because of the #login_required decorator, appending the analyzer method url as the next parameter.
u = User(imei, name, password, uid)
Here, you're not performing a lookup, you're making a new User object. Instead of making a new object, you'll need to get the User from your database and pass that to the login_user method. Documentation for the method here.