Authentication and authorization concepts explained using python. Basic authentication

Authentication and authorization concepts explained using python. Basic authentication
Authentication - proving user's identity.
Authorization - granting user access to a resource.
TL;DR

Let's imagine we are back in time when there is no powerful frameworks in the python ecosystem. As a developer you have written your own very small and easy framework to create your own API.  Framework provides request and response classes as well as ability to connect functions with urls. You are trying to implement note taking API(let's assume json exists in this time).  So, you write a simple function which consumes request as input and returns response as output. A client makes request with note data and creates a new note via API.

def create_note(request: Request) -> Response:
    note = note_service.create_note(data=request.data)
    return Response(data=note, status=201) 
api.py

The problem with the endpoint is that we are not associating a user with the request. Anybody from the internet can hit our API as anon user and create a note.

Now we will introduce new concept for our application – identification and authentication.

Identification is the ability to identify uniquely a user of a system or an application that is running in the system. For example, user provides his email. Then system identifies him in the database.
Identification

Now let's assume  the user includes his email in json while doing post request.
Like this:

{
	"credentials": {
    	"email": "dev@gmail.com"
    },
	"title": "My first note",
	"text": "My first note via api"
}
input data in json
0:00
/

At the backend we can implement some sort of extracting user function out of post data. This is called identification.

def identify_user(request: Request) -> Optional[User]:
	# request.data represents such structure:
	#	{
    #		"credentials" {
    #			"email": "dev@gmail.com",
    #		},
	#		
    #		"title": "My first note",
	#		"text": "My first note via api"
	#	}

	credentials: dict = request.data.get('credentials')
	email: str = credentials.get('email')
	return User.objects.filter(email=email).first()
identification.py

In the function above we read email from credentials object. Then we search for a user with specified email in database. If user not found None will be returned. Let's use this is in our API endpoint.

def create_note(request: Request) -> Response:
    user = identify_user(request=request)
    if user is None:
        return Response(data={"msg": "Wrong credentials"}, status=401)

    note = note_service.create_note(data=request.data)
    return Response(data=note, status=201) 
api.py

Now we will introduce the concept of  authentication.

Authentication is the ability to prove that a user or application is genuinely who that person or what that application claims to be. Authentication includes identification. For example, user provides his email and password. The system uses email to identify user and password to confirm that the user is really who he is.
Authentication

0:00
/


def authenticate(request: Request) -> Optional[User]:
	# request.data represents such structure:
	#	{
    #		"credentials" {
    #			"email": "dev@gmail.com",
    #           "password": "1234test"
    #		},
	#		
    #		"title": "My first note",
	#		"text": "My first note via api"
	#	}

	credentials: dict = request.data.get('credentials')
	email: str = credentials.get('email')
    password: str = credentials.get('password')
    
    # Wrong email
    user: Optional[User] = identify_user(request=request)
    if user is None:
        return None
    
    # Wrong password
    if hash_password(password) != user.password:
        return None
    
	return user
authentification.py
def create_note(request: Request) -> Response:
    user = authenticate(request=request)
    if user is None:
        return Response(data={"msg": "Wrong credentials"}, status=401)

    note = note_service.create_note(data=request.data)
    return Response(data=note, status=201) 
api.py


Now we would like to move credentials from sending each time in body. Why? Because for some HTTP requests you don't even use json. For example, GET http method is usually used to retrieve resources and doesn't include body. Let's look get a closer look into HTTP messages.

POST HTTP request

HTTP message consists of HTTP method, path, HTTP version, headers and body.

HTTP method describes what action we would like to make – receive, send, change data(GET, POST, PATCH).

We can put credentials info into headers. We can search for a list of HTTP headers and their description. For example, such information can be found here https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers

We are interested in Authentication section. Here you can see special header called Authorization where we can put credentials for authentication.

Let's move our credentials into Authorization header in such way.

GET HTTP request with authorization header

Now we're introducing new concept – Authorization

Authorization is the process of giving someone the ability to access a resource. For example, we would like to allow only logged-in users to be able to use our API. So, a user should be authorized to interact with endpoints
Authorization
0:00
/
Successful authorization

We should also look into WWW-Authenticate header.

As said on developer.mozilla :

The HTTP WWW-Authenticate response header defines the HTTP authentication methods ("challenges") that might be used to gain access to a specific resource.

So, if user authorization failed we should return this header with specified authentication method.
Why are we using such headers? Because it's a standard. Standards allow to build the whole ecosystem and spread various interchangeable integrations. We would like to follow this trend and build our systems following the global standards.

What kind of method are we using? Currently we use our custom method. But we can find that it's very similar to basic authentication.

Basic authentication!
As a developer you understand that any standard is a good thing.
You see that now you should place Basic <base64-encoded-string inside of Authorization header.  We should also return WWW-Authenticate : Basic when it fails. Let's make changes into our code.

def authenticate(request: Request) -> Optional[User]:
    authorization_header = request.headers.get('Authorization')
    if authorization_header is None:
        return None
    
    # Basic <encoded_value>
    scheme, credentials = authorization_header.split(' ')
    credentials = base64.b64decode(credentials).decode('utf-8')
	email, password = credentials.split(':')
    
    # Wrong email
    user: Optional[User] = identify_user(request=request)
    if user is None:
        return None
    
    # Wrong password
    if hash_password(password) != user.password:
        return None
    
	return user
authentification.py
def create_note(request: Request) -> Response:
    user = authenticate(request=request)
    if user is None:
        response Response(
            data={"msg": "Wrong credentials"}, 
            status=status.HTTP_401_UNATHORIZED,
            headers={"WWW-Authenticate": "Basic"},
        )

    note = note_service.create_note(data=request.data)
    return Response(data=note, status=201) 
api.py
0:00
/
Failed authorization

Congratulations! You created basic authentication in your app from scratch.

Loking inside other frameworks

The knowledge you gained allows to understand concepts behind authentication and authorization in other frameworks used in python ecosystem to build Web API.

Django

django-rest-framework/authentication.py at master · encode/django-rest-framework
Web APIs for Django. 🎸. Contribute to encode/django-rest-framework development by creating an account on GitHub.
class BasicAuthentication(BaseAuthentication):
    """
    HTTP Basic authentication against username/password.
    """
    www_authenticate_realm = 'api'

    def authenticate(self, request):
        """
        Returns a `User` if a correct username and password have been supplied
        using HTTP Basic authentication.  Otherwise returns `None`.
        """
        auth = get_authorization_header(request).split()

        if not auth or auth[0].lower() != b'basic':
            return None

        if len(auth) == 1:
            msg = _('Invalid basic header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid basic header. Credentials string should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            try:
                auth_decoded = base64.b64decode(auth[1]).decode('utf-8')
            except UnicodeDecodeError:
                auth_decoded = base64.b64decode(auth[1]).decode('latin-1')
            auth_parts = auth_decoded.partition(':')
        except (TypeError, UnicodeDecodeError, binascii.Error):
            msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
            raise exceptions.AuthenticationFailed(msg)

        userid, password = auth_parts[0], auth_parts[2]
        return self.authenticate_credentials(userid, password, request)

    def authenticate_credentials(self, userid, password, request=None):
        """
        Authenticate the userid and password against username and password
        with optional request for context.
        """
        credentials = {
            get_user_model().USERNAME_FIELD: userid,
            'password': password
        }
        user = authenticate(request=request, **credentials)

        if user is None:
            raise exceptions.AuthenticationFailed(_('Invalid username/password.'))

        if not user.is_active:
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

        return (user, None)

    def authenticate_header(self, request):
        return 'Basic realm="%s"' % self.www_authenticate_realm

Flask

Flask-HTTPAuth/flask_httpauth.py at main · miguelgrinberg/Flask-HTTPAuth
Simple extension that provides Basic, Digest and Token HTTP authentication for Flask routes - Flask-HTTPAuth/flask_httpauth.py at main · miguelgrinberg/Flask-HTTPAuth
class HTTPBasicAuth(HTTPAuth):
    def __init__(self, scheme=None, realm=None):
        super(HTTPBasicAuth, self).__init__(scheme or 'Basic', realm)

        self.hash_password_callback = None
        self.verify_password_callback = None

    def hash_password(self, f):
        self.hash_password_callback = f
        return f

    def verify_password(self, f):
        self.verify_password_callback = f
        return f

    def get_auth(self):
        # this version of the Authorization header parser is more flexible
        # than Werkzeug's, as it also accepts other schemes besides "Basic"
        header = self.header or 'Authorization'
        if header not in request.headers:
            return None
        value = request.headers[header].encode('utf-8')
        try:
            scheme, credentials = value.split(b' ', 1)
            username, password = b64decode(credentials).split(b':', 1)
        except (ValueError, TypeError):
            return None
        try:
            username = username.decode('utf-8')
            password = password.decode('utf-8')
        except UnicodeDecodeError:
            username = None
            password = None
        return Authorization(
            scheme, {'username': username, 'password': password})

    def authenticate(self, auth, stored_password):
        if auth:
            username = auth.username
            client_password = auth.password
        else:
            username = ""
            client_password = ""
        if self.verify_password_callback:
            return self.ensure_sync(self.verify_password_callback)(
                username, client_password)
        if not auth:
            return
        if self.hash_password_callback:
            try:
                client_password = self.ensure_sync(
                    self.hash_password_callback)(client_password)
            except TypeError:
                client_password = self.ensure_sync(
                    self.hash_password_callback)(username, client_password)
        return auth.username if client_password is not None and \
            stored_password is not None and \
            hmac.compare_digest(client_password, stored_password) else None

FastAPI

fastapi/http.py at 3b2e891917dd2739659643cedb5258e65d54934d · tiangolo/fastapi
FastAPI framework, high performance, easy to learn, fast to code, ready for production - fastapi/http.py at 3b2e891917dd2739659643cedb5258e65d54934d · tiangolo/fastapi
class HTTPBasic(HTTPBase):
    def __init__(
        self,
        *,
        scheme_name: Optional[str] = None,
        realm: Optional[str] = None,
        description: Optional[str] = None,
        auto_error: bool = True,
    ):
        self.model = HTTPBaseModel(scheme="basic", description=description)
        self.scheme_name = scheme_name or self.__class__.__name__
        self.realm = realm
        self.auto_error = auto_error

    async def __call__(  # type: ignore
        self, request: Request
    ) -> Optional[HTTPBasicCredentials]:
        authorization: str = request.headers.get("Authorization")
        scheme, param = get_authorization_scheme_param(authorization)
        if self.realm:
            unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
        else:
            unauthorized_headers = {"WWW-Authenticate": "Basic"}
        invalid_user_credentials_exc = HTTPException(
            status_code=HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication credentials",
            headers=unauthorized_headers,
        )
        if not authorization or scheme.lower() != "basic":
            if self.auto_error:
                raise HTTPException(
                    status_code=HTTP_401_UNAUTHORIZED,
                    detail="Not authenticated",
                    headers=unauthorized_headers,
                )
            else:
                return None
        try:
            data = b64decode(param).decode("ascii")
        except (ValueError, UnicodeDecodeError, binascii.Error):
            raise invalid_user_credentials_exc
        username, separator, password = data.partition(":")
        if not separator:
            raise invalid_user_credentials_exc
        return HTTPBasicCredentials(username=username, password=password)

Conclusion

You learned basic concepts of authentication and authorization. New skills will allow you to create other kind of authentication systems like JWT based authentication, session authentication, etc.

Recap

  • Identification is the ability to identify uniquely a user of a system or an application that is running in the system. For example, we search user's email in database to identify him.
  • Authentication is the ability to prove that a user or application is genuinely who that person or what that application claims to be. Authentication includes identification. For example, user provides his email and password and then user is searched in the db, passwords compared.
  • Basic authentication credentials are stored inside Authorization header.
  • Authorization is the process of giving someone the ability to access a resource. For example, we would like to allow only logged-in users to be able to use our API. So, a user should be authorized to interact with endpoints