Skip to content

Commit 75ac0f2

Browse files
committed
Added authentication logic, AuthenticationError, UNAUTHORIZED_401 status
1 parent 0fff602 commit 75ac0f2

File tree

4 files changed

+85
-1
lines changed

4 files changed

+85
-1
lines changed

adafruit_httpserver/authentication.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2022 Dan Halbert for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
"""
5+
`adafruit_httpserver.authentication`
6+
====================================================
7+
* Author(s): Michał Pokusa
8+
"""
9+
10+
try:
11+
from typing import Union, List
12+
except ImportError:
13+
pass
14+
15+
from binascii import b2a_base64
16+
17+
from .exceptions import AuthenticationError
18+
from .request import HTTPRequest
19+
20+
21+
class Basic:
22+
"""Represents HTTP Basic Authentication."""
23+
24+
def __init__(self, username: str, password: str) -> None:
25+
self._value = b2a_base64(f"{username}:{password}".encode()).decode().strip()
26+
27+
def __str__(self) -> str:
28+
return f"Basic {self._value}"
29+
30+
31+
class Bearer:
32+
"""Represents HTTP Bearer Token Authentication."""
33+
34+
def __init__(self, token: str) -> None:
35+
self._value = token
36+
37+
def __str__(self) -> str:
38+
return f"Bearer {self._value}"
39+
40+
41+
def check_authentication(
42+
request: HTTPRequest, auths: List[Union[Basic, Bearer]]
43+
) -> bool:
44+
"""
45+
Returns ``True`` if request is authorized by any of the authentications, ``False`` otherwise.
46+
"""
47+
48+
auth_header = request.headers.get("Authorization")
49+
50+
if auth_header is None:
51+
return False
52+
53+
return any(auth_header == str(auth) for auth in auths)
54+
55+
56+
def require_authentication(
57+
request: HTTPRequest, auths: List[Union[Basic, Bearer]]
58+
) -> None:
59+
"""
60+
Checks if the request is authorized and raises ``AuthenticationError`` if not.
61+
62+
If the error is not caught, the server will return ``401 Unauthorized``.
63+
"""
64+
65+
if not check_authentication(request, auths):
66+
raise AuthenticationError(
67+
"Request is not authenticated by any of the provided authentications"
68+
)

adafruit_httpserver/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
"""
99

1010

11+
class AuthenticationError(Exception):
12+
"""
13+
Raised by ``require_authentication`` when the ``HTTPRequest`` is not authorized.
14+
"""
15+
16+
1117
class InvalidPathError(Exception):
1218
"""
1319
Parent class for all path related errors.

adafruit_httpserver/server.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from errno import EAGAIN, ECONNRESET, ETIMEDOUT
1818

19-
from .exceptions import FileNotExistsError, InvalidPathError
19+
from .exceptions import AuthenticationError, FileNotExistsError, InvalidPathError
2020
from .methods import HTTPMethod
2121
from .request import HTTPRequest
2222
from .response import HTTPResponse
@@ -185,6 +185,13 @@ def poll(self):
185185
request, status=CommonHTTPStatus.BAD_REQUEST_400
186186
).send()
187187

188+
except AuthenticationError:
189+
HTTPResponse(
190+
request,
191+
status=CommonHTTPStatus.UNAUTHORIZED_401,
192+
headers={"WWW-Authenticate": 'Basic charset="UTF-8"'},
193+
).send()
194+
188195
except InvalidPathError as error:
189196
HTTPResponse(request, status=CommonHTTPStatus.FORBIDDEN_403).send(
190197
str(error)

adafruit_httpserver/status.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class CommonHTTPStatus(HTTPStatus): # pylint: disable=too-few-public-methods
3939
BAD_REQUEST_400 = HTTPStatus(400, "Bad Request")
4040
"""400 Bad Request"""
4141

42+
UNAUTHORIZED_401 = HTTPStatus(401, "Unauthorized")
43+
"""401 Unauthorized"""
44+
4245
FORBIDDEN_403 = HTTPStatus(403, "Forbidden")
4346
"""403 Forbidden"""
4447

0 commit comments

Comments
 (0)