Flask/Python Web Auth made #ez!
Tags: tutorial, webdev, codenewbie, python
Published: 2020-08-07 06:40:05

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](https://www.npmjs.com/package/express-jwt) (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](https://cloud.mongodb.com), your database access URL will be something like:
```
mongodb+srv://username:password@...
```
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:
```shell
mkdir flask-auth-test && cd flask-auth-test
# Add to .gitignore just in case you want to push
echo "backend/venv" >> .gitignore
```

Now, we need to set up our flask app.
```shell
# 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
```

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

```python
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!")
```

Now, let's run the app with:
```python
export FLASK_DEBUG=1
python3 -m flask run
```

Then, if we perform a `GET` request to `http://localhost:5000/` then, you will get this output:
```shell
$ curl http://localhost:5000
"works"
```

Now, let's do the database.
```python
# ...
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()
```

Alright! We can create our database!
```shell
python3 -m flask migrate
```
You should now see a new file called `database.db` in the folder. Nice!

### Adding authentication
Let's now add authentication to our app.
```python
# 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
```

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

```python
#...
@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})
```

We don't need a login route because `flask-httpauth` handles that for us. Now, let's add a protected route.
```python
@app.route("/secret")
@auth.login_required
def secret():
    return jsonify("never gonna give you up")
```
The `/secret` route is now protected and can not be accessed by an unauthorized user. Now, let's try registering:
```shell
curl -X POST -H "Content-Type: application/json" -d '{"email: "aaaaa@aaaa.aaa", "password": "a"}' http://localhost:5000/register
```
And you should get `{"success": True}`!

Nice!

Now, if you want to access the `/secret` route, you just add the user like this:
```shell
# email:password
curl -u aaaaa@aaaa.aaa:a http://localhost:5000
```
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:
```shell
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(() => {/*...*/});
```

## Full code

```python
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")

```

## 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