Authentication and authorization concepts explained using python. Basic authentication

Authorization - granting user access to a resource.
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)
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.
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"
}
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()
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)
Now we will introduce the concept of authentication.
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
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)
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.

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.

Now we're introducing new concept – 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
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)
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
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
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
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