DEV Community

arnu515
arnu515

Posted on

 

Flask/Python Web Auth made #ez!

If you've want to add authentication to your backend, you would use token-based authentication like JWT. JWT stands for jsonwebtoken and is an encrypted token which can contain some payload data like the user id which can be used to perform actions as that user.

The thing with JWT is that you will have to handle expiration, refreshing and blacklisting. Well, there are some libraries like express-jwt link (for node backends) that make it easier, but, authentication is a supported/built in part of a url!

Don't know what I mean? Well, if you've worked with MongoDB Atlas, your database access URL will be something like:

mongodb+srv://username:password@...
Enter fullscreen mode Exit fullscreen mode

So you see, you send in your username and password along with the URL.

Well, but how do I authenticate the user that way?

I hear you ask? Well, that's the point of this tutorial.

Project setup

Let's setup our flask project.

First, let's create a folder for our app:

mkdir flask-auth-test && cd flask-auth-test
# Add to .gitignore just in case you want to push
echo "backend/venv" >> .gitignore
Enter fullscreen mode Exit fullscreen mode

Now, we need to set up our flask app.

# Create a virtual environment
# pip3 install venv 
# OR
# sudo apt install python3-venv
# for debian/ubuntu systems if the below command fails
python3 -m venv venv
# Activate the venv
source venv/bin/activate
# FOR FISH: source venv/bin/activate.fish
# Windows: .\venv\Scripts\activate
pip install flask flask-sqlalchemy flask-httpauth flask-cors python-dotenv
touch app.py
Enter fullscreen mode Exit fullscreen mode

Setup app

First, let's finish with the boiler-platey code.

from flask import Flask, jsonify, request, g
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
CORS(app)

@app.cli.command("migrate")
def migrate_cmd():
    db.create_all()

@app.route('/')
def index():
    return jsonify("works!")
Enter fullscreen mode Exit fullscreen mode

Now, let's run the app with:

export FLASK_DEBUG=1
python3 -m flask run
Enter fullscreen mode Exit fullscreen mode

Then, if we perform a GET request to http://localhost:5000/ then, you will get this output:

$ curl http://localhost:5000
"works"
Enter fullscreen mode Exit fullscreen mode

Now, let's do the database.

# ...
class User(db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String)
    username = db.Column(db.String)
    password = db.Column(db.String)

    def save(self):
        db.session.add(self)
        db.session.commit()

    def delete(self):
        db.session.delete(self)
        db.session.commit()
Enter fullscreen mode Exit fullscreen mode

Alright! We can create our database!

python3 -m flask migrate
Enter fullscreen mode Exit fullscreen mode

You should now see a new file called database.db in the folder. Nice!

Adding authentication

Let's now add authentication to our app.

# add this import at the top
from flask_httpauth import HTTPBasicAuth

# ...

# now add this under Cors(App)
auth = HTTPBasicAuth()

# This is used to verify if the password is correct
@auth.verify_password
def verify_password(email: str, password: str):
    user = User.query.filter_by(email=email)
    if not user:
        return False
    # Add the user to global variables
    g.user = user
    return True
Enter fullscreen mode Exit fullscreen mode

Now that the authentication is secure, let's add a register route.

#...
@app.route("/register", methods=["POST"])
def register():
    email = request.json.get("email").lower()
    password = request.json.get("password")
    if not email or not password:
        return jsonify({"success": False, "message": "One or more fields empty or not present"}), 400
    if not re.match(pattern=r"[\w_.]{3,}@\w{3,}\.\w{2,}", string=email):
        return jsonify({"success": False, "message": "Invalid email"}), 400
    user = User.query.filter_by(email=email)
    if user:
        return jsonify({"success": False, "message": "Account already exists! Choose a different email or username"}), 400
    u = User(email=email, password=password)
    u.save()
    return jsonify({"success": True})
Enter fullscreen mode Exit fullscreen mode

We don't need a login route because flask-httpauth handles that for us. Now, let's add a protected route.

@app.route("/secret")
@auth.login_required
def secret():
    return jsonify("never gonna give you up")
Enter fullscreen mode Exit fullscreen mode

The /secret route is now protected and can not be accessed by an unauthorized user. Now, let's try registering:

curl -X POST -H "Content-Type: application/json" -d '{"email: "aaaaa@aaaa.aaa", "password": "a"}' http://localhost:5000/register
Enter fullscreen mode Exit fullscreen mode

And you should get {"success": True}!

Nice!

Now, if you want to access the /secret route, you just add the user like this:

# email:password
curl -u aaaaa@aaaa.aaa:a http://localhost:5000
Enter fullscreen mode Exit fullscreen mode

And if everything went well, you should get rickrolled!


Using fetch

But how do I use it in my frontend?
If your frontend uses javascript (which it most likely does), you can fetch the backend like so:

fetch("http://localhost:5000/secret", {
    headers: {
        # The btoa method encodes the auth string and is available in all browsers.
        Authorization: `Basic ${btoa("aaaaa@aaaa.aaa:a")`
    }
}).then(() => {/*...*/});
Enter fullscreen mode Exit fullscreen mode

Full code

from flask import Flask, jsonify, request, g
from flask_httpauth import HTTPBasicAuth
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
CORS(app)
auth = HTTPBasicAuth()

@app.cli.command("migrate")
def migrate_cmd():
    db.create_all()

# This is used to verify if the password is correct
@auth.verify_password
def verify_password(email: str, password: str):
    user = User.query.filter_by(email=email)
    if not user:
        return False
    # Add the user to global variables
    g.user = user
    return True

# DB
class User(db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String)
    username = db.Column(db.String)
    password = db.Column(db.String)

    def save(self):
        db.session.add(self)
        db.session.commit()

    def delete(self):
        db.session.delete(self)
        db.session.commit()

# Routes
@app.route("/register", methods=["POST"])
def register():
    email = request.json.get("email").lower()
    password = request.json.get("password")
    if not email or not password:
        return jsonify({"success": False, "message": "One or more fields empty or not present"}), 400
    if not re.match(pattern=r"[\w_.]{3,}@\w{3,}\.\w{2,}", string=email):
        return jsonify({"success": False, "message": "Invalid email"}), 400
    user = User.query.filter_by(email=email)
    if user:
        return jsonify({"success": False, "message": "Account already exists! Choose a different email or username"}), 400
    u = User(email=email, password=password)
    u.save()
    return jsonify({"success": True})


@app.route('/')
def index():
    return jsonify("works!")

@app.route("/secret")
@auth.login_required
def secret():
    return jsonify("never gonna give you up")

Enter fullscreen mode Exit fullscreen mode

Conclusion

This method of authentication is much easier than using JWT. Hope you've learned something, and of course, correct my mistakes in the comments! :P

Top comments (0)