Authentication intro & HTTP Basic Auth


There are many ways to handle security, authentication and authorization.

And it normally is a complex and "difficult" topic.

In many frameworks and systems just handling security and authentication takes a big amount of effort and code (in many cases it can be 50% or more of all the code written).

FastAPI provides several tools to help you deal with Security easily, rapidly, in a standard way, without having to study and learn all the security specifications.

But first, let's check some small concepts.

Security schemes within OpenAPI

OpenAPI (previously known as Swagger) is the open specification for building APIs. That's what makes it possible to have multiple automatic interactive documentation interfaces, code generation, etc.

OpenAPI has a way to define multiple security "schemes". By using them, you can take advantage of all these standard-based tools, including these interactive documentation systems.

OpenAPI defines the following security schemes:

  • apiKey: an application specific key that can come from:
    • A query parameter.
    • A header.
    • A cookie.
  • http: standard HTTP authentication systems, including:
    • bearer: a header Authorization with a value of Bearer plus a token. This is inherited from OAuth2.
    • HTTP Basic authentication.
    • HTTP Digest, etc.
  • oauth2: all the OAuth2 ways to handle security (called "flows").
    • Several of these flows are appropriate for building an OAuth 2.0 authentication provider (like Google, Facebook, Twitter, GitHub, etc):
      • implicit
      • clientCredentials
      • authorizationCode
    • But there is one specific "flow" that can be perfectly used for handling authentication in the same application directly:
      • password: some next chapters will cover examples of this.
  • openIdConnect: has a way to define how to discover OAuth2 authentication data automatically.
    • This automatic discovery is what is defined in the OpenID Connect specification.

Auth providers in FastAPI

Integrating other authentication/authorization providers like Google, Facebook, Twitter, GitHub, etc. is also possible and relatively easy. The most complex problem is building an authentication/authorization provider like those, but FastAPI gives you the tools to do it easily, while doing the heavy lifting for you.

OAuth2

OAuth2 is a specification that defines several ways to handle authentication and authorization. It is quite an extensive specification and covers several complex use cases.

It includes ways to authenticate using a "third party". That's what all the systems with "login with Facebook, Google, Twitter, GitHub" use underneath.

OAuth 1

There was an OAuth 1, which is very different from OAuth2, and more complex, as it included directly specifications on how to encrypt the communication.

As opposed to that, OAuth2 doesn't specify how to encrypt the communication, it expects you to have your application served with HTTPS.

It is not very popular or used nowadays.

OpenID Connect

OpenID Connect is another specification, based on OAuth2.

It just extends OAuth2 specifying some things that are relatively ambiguous in OAuth2, to try to make it more interoperable.

For example, Google login itself uses OpenID Connect (which underneath uses OAuth2). Facebook login on the other hand doesn't support OpenID Connect. It has its own flavor of OAuth2.

OpenID (not "OpenID Connect")

There was also an "OpenID" specification. That tried to solve the same thing as OpenID Connect, but was not based on OAuth2.

So, it was a complete additional system. It is not very popular or used nowadays.

HTTP Basic Auth

For the simplest cases, you can use HTTP Basic Auth.

In HTTP Basic Auth, the application expects a header that contains a username and a password, and returns a header WWW-Authenticate with a value of Basic, and an optional realm parameter.

That tells the browser to show the integrated prompt for a username and password. Then, when you type that username and password, the browser sends them in the header automatically.

If it doesn't receive a header that contains a username and a password, it returns an HTTP 401 "Unauthorized" error.

HTTP Basic Auth: Setting up basic security

We will use HTTP Basic Auth to learn some basic structures and techniques. To start implementing it:

  • Import HTTPBasic and HTTPBasicCredentials.
  • Create a "security scheme" using HTTPBasic.
  • Use that security with a dependency in your path operation.
  • It returns an object of type HTTPBasicCredentials:
    • This contains the username and password sent.
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


@app.get("/users/me")
def read_current_user(credentials: HTTPBasicCredentials = Depends(security)):
    return {"username": credentials.username, "password": credentials.password}

 



 



 

When you try to open the URL for the first time (or click the "Execute" button in the docs) the browser will ask you for your username and password:

Prompt

Checking the credentials

Here's a more complete example where we actually check the credentials.

If we detect that the credentials are incorrect, we return an HTTPException with a status code 401 (the same returned when no credentials are provided) and add the header WWW-Authenticate to make the browser show the login prompt again:

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
    if not (credentials.username == "admin@mysite.com") or not (credentials.password == "swordfish"):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
    return {"username": username}








 
 
 
 
 
 
 
 





Securing against timing attacks

Using a simple check like the following to check credentials can feel too simple:

if not (credentials.username == "admin@mysite.com") or not (credentials.password == "swordfish"):
    # Return some error
    ...

What is a timing attack?

That's because it is too simple, and not yet secured against something such as a timing attack. But what's a "timing attack"?

Let's imagine some attackers are trying to guess the username and password.

And they send a request with a username johndoe@gmail.com and a password love123.

Then the Python logic your application would run be equivalent to something like:

if "johndoe@gmail.com" == "admin@mysite.com" and "love123" == "swordfish":
    ...

But right at the moment Python compares the first j in johndoe@gmail.com to the first a in admin@mysite.com, it will return False, because it already knows that those two strings are not the same, thinking that "there's no need to waste more computation comparing the rest of the letters". And your application will say "incorrect user or password".

But then the attackers try with username admin@mysite.cn and password love123.

And your application code does something like:

if "admin@mysite.cn" == "admin@mysite.com" and "love123" == "swordfish":
    ...

Python will have to compare the whole admin@mysite.c in both admin@mysite.cn and admin@mysite.com before realizing that both strings are not the same. So it will take some extra microseconds to reply back "incorrect user or password".

The time to answer helps the attackers

At that point, by noticing that the server took some microseconds longer to send the "incorrect user or password" response, the attackers will know that they got something right, some of the initial letters were right.

And then they can try again knowing that it's probably something more similar to admin@mysite.cn than to johndoe@gmail.com.

Of course, the attackers would not try all this by hand, they would write a program to do it, possibly with thousands or millions of tests per second. And would get just one extra correct letter at a time.

But doing that, in some minutes or hours the attackers would have guessed the correct username and password, with the "help" of our application, just using the time taken to answer.

Fix it with secrets.compare_digest()

In our code we are actually going to use secrets.compare_digest() to secure against this whole range of security attacks.

In short, it will take the same time to compare admin@mysite.cn to admin@mysite.com than it takes to compare johndoe@gmail.com to admin@mysite.com. And the same for the password.

For this, use the Python standard module secrets to check the username and password.

secrets.compare_digest() needs to take bytes or a str that only contains ASCII characters (the ones in English), this means it wouldn't work with characters like á, as in Sebastián.

To handle that, we first convert the username and password to bytes encoding them with UTF-8.

Then we can use secrets.compare_digest() to ensure that credentials.username is "admin@mysite.com", and that credentials.password is "swordfish":

import secrets

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = b"admin@mysite.com"
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = b"swordfish"
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
    return {"username": username}
 










 
 
 
 
 
 
 
 
 
 
 











😱 Downsides of HTTP Basic Auth

Basic Auth only requires a user’s credentials to gain access to their online account. The account user’s credentials are sent from the “every request”. Although this process is straightforward, it can leave your credentials and, eventually, your online account vulnerable if you don't take extra steps:

  • Secure your connection through transport layer security (TLS), i.e. HTTPS. Otherwise your password may be compromised as it's sent in base64 encoded plaintext through the headers.
  • Set up multi-factor authentication (MFA), as typically used with Basic Auth. Otherwise there are no additional layers of security to prevent people who now have your credentials from accessing your account whenever they want.

Thus Basic Auth is an authentication protocol, which mainly focuses on proving that you're the correct person because you know your password. This can leave your private information vulnerable, especially if your internet connection isn’t secured through TLS or you don’t set up MFA.

To ensure better protection of your online accounts, OAuth is the often the modern way to go because, unlike Basic Auth, it doesn’t give away your password every request. That’s because OAuth is more of an authorization framework. OAuth uses authorization tokens to verify an identity between consumers and service providers. OAuth lets users authenticate with one service (for example GitHub or Google) and use the token they receive through there to access secured resources on other sites and services.

This doesn't mean that HTTPS and MFA can be left behind!

Inspecting the credentials in a HTTP Basic Auth message

We can inspect the Authentication header ourselves and retrieve our password from it. Take the following steps:

  • Start up the example HTTP Basic Auth application
  • Go to the /docs of the API and use it to submit the credentials and perform a request.
  • Right-click on the webpage and open up the Inspect tool of Google Chrome.
  • Go to the Network tab and select the Fetch/XHR filter

Inspecting 1

Now we are ready to inspect the next requests:

  • Press ´Execute´ again on the API page
  • Click open the request that appears in the inspection tool
  • Copy the encoded part inside the Authentication header, after Basic

Inspecting 2

All that's left is to decode the base64 encoded credentials:

Inspecting 3

As you can see we can quite easily retrieve our credentials. If someone were to intercept our request they would be able to do the same thing, as we are not using transport layer security (TLS), i.e. HTTPS.

This method using the inspection tool of Google Chrome will still work on our (the client's) end even when we are using HTTPS. This is because when it appears in our tool it is already decrypted. The major difference will be that the request will be encrypted in-transit. Luckily Oketeto handles HTTPS for us.

Last update: 11/28/2022, 8:45:13 PM