Basic Authentication & JWT

In this article I show how to secure a Flask backend with Basic Authentication and also with JWT. I will show the usage of the library flask-http-auth for the Basic Authentication and the token based Authentication. Then, I will add the library pyjwt to encode and decode a JWT.

Content

  • Basic Authentication
  • Token Based Authentication
  • JWT

For more details, watch this video.

You can find all the code of the article in this repository.

Let’s start with an already created flask application that has two endpoints: the health check and another one to get all the user’s information. I want to secure only the one which returns the user’s information. I will start by adding the dependency Flask-HTTPAuth using Poetry.

> poetry add Flask-HTTPAuth

And now, I will add some methods to check the password in the __init__.py file which is at the top level of the routes.

I will hard code the password in this module, but I should preferably have them stored in a database. I will talk about the database connection in another article.

Basic Authentication

The basic authentication consists in sending the username and password for each request. They will be encoded in base64 and sent in the authorization header. This is one of the simplest authentication method, because it sends practically the credentials visible to anyone. Let’s implement it.

allowed_users = {
    "sergio": generate_password_hash("my-password"),
    "bob": generate_password_hash("his-password"),
    }

basic_auth = HTTPBasicAuth()

@basic_auth.verify_password
def verify_basic_password(username, password):
    if username not in allowed_users:
        return None

    if check_password_hash(allowed_users[username], password):
        return username

I’ve added the decorator to verify the password. This decorator will be executed at each request before entering in each endpoint. And now add a decorator to protect the endpoints.

@users_bp.route("", methods=["GET"])
@basic_auth.login_required
def get_all_users():
    all_users = [{"id": 1, "name": "joe"}, {"id": 2, "name": "bob"}]
    return jsonify(all_users)

To use my endpoints, I must now request it like that.

> curl localhost:5000/users/ -u "sergio:my-password"

With cUrl, the -u option will encode in base64 the username and password sent, and put them in the Authorization header. It will be the same as the following.

> curl localhost:5000/users/ -H "Authorization: Basic c2VyZ2lvOm15LXBhc3N3b3Jk"

But the basic authentication is basic. It can be easily intercepted in HTTP communications. And the base64 can be decoded easily. No digest message is needed. So let’s change this to the next step with the token based authentication.

Token Based Authentication

Let’s enrich my __init__.py module to handle both authentications.

allowed_tokens = {
    "token-sergio": "sergio",
    "token-bob": "bob",
    }

token_auth = HTTPTokenAuth(scheme="Bearer")

@token_auth.verify_token
def verify_token(token):
    if token not in allowed_tokens:
        return None
    return allowed_tokens[token]

And now protect the desired route.

@users_bp.route("", methods=["GET"])
@token_auth.login_required
def get_all_users():
    all_users = [{"id": 1, "name": "joe"}, {"id": 2, "name": "bob"}]
    return jsonify(all_users)

And finally try it.

> curl localhost:5000/users -H "Authorization: Bearer token-sergio"

I’ve added my authorization header with the Bearer prefix, as indicated in the configuration of the token, and then the token associated with my user. This authentication method is more secure than the basic one. Why? I can generate tokens randomly with UUIDs or hashes, and apply validation times to them. This way, even if a token is intercepted, it will be invalidated soonly. Of course, here i’ve just hard coded the token, which isn’t good, but I can use a JWT that isn’t stored anywhere but generated on the server side. But what is a JWT?

JWT

A JWT is a JSON Web Token. It’s a JSON document that is encrypted with a secret key. First, the user must authenticate itself within a form or with the basic authentication. This will be the only request where the username and password are sent. Then, on the server side, a JSON document will be created with some useful information about the user, like its ID, its name or some other information.

This document will be encrypted with an expiration date. This way, even if the token is intercepted, it won’t be used forever.

What information can I store in the document? I should store all the necessary information of the user to avoid requests to the database, to avoid storing any information in the session. As the token was generated in the server side, with a secret key, it won’t be modified by anyone else. I can trust the information stored in this token.

Let’s see it. I must first add the pyjwt dependency.

> poetry add pyjwt

And now, I will create another route to create the JWT tokens.

@auth_bp.route("/login", methods=["POST"])
def login():
    d = request.json
    if "username" not in d or "password" not in d:
        raise Exception("Unable to authenticate")
    if not check_password_hash(allowed_users[d["username"]], d["password"]):
        raise Exception("Invalid password")

    encoded_jwt = jwt.encode({"sub": 1, "name": "sergio", "exp": datetime.datetime.now(tz=timezone.utc) + datetime.timedelta(minutes=30)}, secret_token, algorithm="HS256")
    return jsonify({"token": encoded_jwt})

This will generate a JWT based on the allowed passwords generated before. Now, I must update the “verify_token” method to verify the JWT instead of the hard-coded tokens.

@token_auth.verify_token
def verify_token(token):
    try:
        decoded_jwt = jwt.decode(token, secret_token, algorithms=["HS256"])
    except Exception as e:
        return None
    if decoded_jwt["name"] in allowed_users:
        return decoded_jwt["name"]
    return None

Let’s test it.

> curl localhost:5000/login -H "content-type: application/json" -d '{"username": "sergio", "password": "my-password"}'
< {"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwibmFtZSI6InNlcmdpbyIsImV4cCI6MTY0NjMxMTAwNH0.YQqB6uLFr1blvdk3ZHasLOD5-E3FRGgyGIZHoy2SBMo"}
>
> curl localhost:5000/users -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwibmFtZSI6InNlcmdpbyIsImV4cCI6MTY0NjMxMTAwNH0.YQqB6uLFr1blvdk3ZHasLOD5-E3FRGgyGIZHoy2SBMo"

Conclusion

  • I’ve added the flask HTTP auth dependency.
  • I’ve implemented the verify password method for the basic authentication, and use the login required decorated on the desired endpoints.
  • I’ve implemented the verified token method for the based authentication.
  • I’ve used the login required decorator on the desired endpoints.
  • I’ve added the pyjwt dependency.
  • I’ve added the login endpoint which generates a JWT with the user information.
  • And i’ve modified the verify token method to decode the JWT and return the user information.

References

Repository


Discover more from The Dev World – Sergio Lema

Subscribe to get the latest posts sent to your email.


Comments

2 responses to “Basic Authentication & JWT”

  1. […] with the information of the authenticated user. This information is used to be sent via a token, a JWT, in the header of the […]

    Like

  2. […] with the information of the authenticated user. This information is used to be sent via a token, a JWT, in the header of the […]

    Like

Leave a comment