Skip to content

Commit

Permalink
Merge pull request #44 from urbanplatform/v2.0.1
Browse files Browse the repository at this point in the history
(v2.0.1) - Minor Bug Fixes and settings documentation
  • Loading branch information
uw-rvitorino authored Oct 29, 2022
2 parents e5015f2 + 87db494 commit 6a8af99
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 30 deletions.
43 changes: 27 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# [WIP] Django Keycloak Authorization
# Django Keycloak Authorization

Middleware to allow authorization using Keycloak and Django for django-rest-framework (DRF) and Graphene-based projects.
This package should only be used in projects starting from scratch, since it overrides the users' management.
Expand Down Expand Up @@ -32,15 +32,35 @@ This package should only be used in projects starting from scratch, since it ove

```python
KEYCLOAK_CONFIG = {
# The Keycloak's Public Server URL (e.g. http://localhost:8080)
'SERVER_URL': '<PUBLIC_SERVER_URL>',
'INTERNAL_URL': '<INTERNAL_SERVER_URL>', # Optional: Default is SERVER_URL
'BASE_PATH': '', # Optional: Default matches Keycloak's default '/auth'
# The Keycloak's Internal URL
# (e.g. http://keycloak:8080 for a docker service named keycloak)
# Optional: Default is SERVER_URL
'INTERNAL_URL': '<INTERNAL_SERVER_URL>',
# Override for default Keycloak's base path
# Default is '/auth/'
'BASE_PATH': '/auth/',
# The name of the Keycloak's realm
'REALM': '<REALM_NAME>',
'CLIENT_ID': '<CLIENT_ID>',
# The ID of this client in the above Keycloak realm
'CLIENT_ID': '<CLIENT_ID>'
# The secret for this confidential client
'CLIENT_SECRET_KEY': '<CLIENT_SECRET_KEY>',
# The name of the admin role for the client
'CLIENT_ADMIN_ROLE': '<CLIENT_ADMIN_ROLE>',
# The name of the admin role for the realm
'REALM_ADMIN_ROLE': '<REALM_ADMIN_ROLE>',
'EXEMPT_URIS': [], # URIS to be ignored by the package
# Regex formatted URLs to skip authentication
'EXEMPT_URIS': [],
# Flag if the token should be introspected or decoded (default is False)
'DECODE_TOKEN': False,
# Flag if the audience in the token should be verified (default is True)
'VERIFY_AUDIENCE': True,
# Flag if the user info has been included in the token (default is True)
'USER_INFO_IN_TOKEN': True,
# Flag to show the traceback of debug logs (default is False)
'TRACE_DEBUG_LOGS': False
}
```

Expand All @@ -54,19 +74,10 @@ This package should only be used in projects starting from scratch, since it ove

```python
REST_FRAMEWORK = {
# ... other rest framework settings.
'DEFAULT_AUTHENTICATION_CLASSES': [
'django_keycloak.authentication.KeycloakAuthentication'
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 100, # Default to 20
'PAGINATE_BY_PARAM': 'page_size',
# Allow client to override, using `?page_size=xxx`.
'MAX_PAGINATE_BY': 100,
# Maximum limit allowed when using `?page_size=xxx`.
'TEST_REQUEST_DEFAULT_FORMAT': 'json'
}
```

Expand Down Expand Up @@ -124,7 +135,7 @@ try to login.
## Notes
Support for celery 5: from version 0.7.4 on we should use celery 5 for the user sync. This implies running celery with celery -A app worker ... instead of celery worker -A app ...
Support for celery 5: from version 0.7.4 on we should use celery 5 for the user sync. This implies running celery with `celery -A app worker ...` instead of `celery worker -A app ...`
## Contact
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "django_uw_keycloak"
version = "2.0.0"
version = "2.0.1"
description = "Middleware to allow authorization using Keycloak and Django"
authors = [
"Ubiwhere <urbanplatform@ubiwhere.com>",
Expand Down
12 changes: 6 additions & 6 deletions src/django_keycloak/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
Module containing custom Django authentication backends.
"""
from typing import Optional, Union
from django.contrib.auth.backends import RemoteUserBackend
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth import get_user_model
from django_keycloak.models import KeycloakUserAutoId, KeycloakUser
from django_keycloak import Token


class KeycloakAuthenticationBackend(RemoteUserBackend):
class KeycloakAuthenticationBackend(BaseBackend):
"""
Custom remote backend for Keycloak
"""

def authenticate(
self,
request,
remote_user: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
):
"""
Expand All @@ -26,15 +26,15 @@ def authenticate(
Parameters
----------
remote_user: str
username: str
The Keycloak's username.
password: str
The Keycloak's password.
"""

# Create token from the provided credentials and check if
# credentials were valid
token = Token.from_credentials(remote_user, password) # type: ignore
token = Token.from_credentials(username, password) # type: ignore

# Check for non-existing or inactive token
if not token:
Expand All @@ -46,7 +46,7 @@ def authenticate(

# try to get user from database
try:
user = User.objects.get(username=remote_user)
user = User.objects.get(username=username)
if isinstance(user, KeycloakUserAutoId):
# Get user information from token
user_info = token.user_info
Expand Down
33 changes: 31 additions & 2 deletions src/django_keycloak/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Settings:
CLIENT_ADMIN_ROLE: str
# The name of the admin role for the realm
REALM_ADMIN_ROLE: str
# Regex formatted URLs to skip authentication for (uses re.match())
# Regex formatted URLs to skip authentication (uses re.match())
EXEMPT_URIS: Optional[List] = field(default_factory=list)
# Overrides SERVER_URL for Keycloak admin calls
INTERNAL_URL: Optional[str] = None
Expand All @@ -43,9 +43,38 @@ class Settings:
# Derived setting of the SERVER/INTERNAL_URL and BASE_PATH
KEYCLOAK_URL: str = field(init=False)

def __force_starting_and_ending_slash(self, string: str) -> str:
"""
Forces a given string to start and end with a slash "/"
Parameters
----------
string: str
A string to force the starting and ending slash.
Returns
-------
str
The transformed string starting and ending with a slash.
"""
if not string.endswith("/"):
string += "/"
if not string.startswith("/"):
string = "/" + string
return string

def __post_init__(self) -> None:
# Decide URL (internal url overrides serverl url)

# Make sure "BASE_PATH" starts and ends with a slash
self.BASE_PATH = self.__force_starting_and_ending_slash(self.BASE_PATH)
# Make sure both "SERVER_URL" and "INTERNAL_URL" don't contain any
# trailing slash
self.SERVER_URL = self.SERVER_URL.rstrip("/")
self.INTERNAL_URL = self.INTERNAL_URL.rstrip("/")

# Decide URL (internal url overrides server url)
URL = self.INTERNAL_URL if self.INTERNAL_URL else self.SERVER_URL

self.KEYCLOAK_URL = f"{URL}{self.BASE_PATH}"


Expand Down
13 changes: 9 additions & 4 deletions src/django_keycloak/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from django_keycloak.config import settings
from django_keycloak.models import KeycloakUser, KeycloakUserAutoId

AUTH_HEADER = "HTTP_AUTHORIZATION"


class KeycloakMiddleware(MiddlewareMixin):
"""
Expand All @@ -27,7 +29,10 @@ def get_token_from_request(self, request) -> Optional[Token]:
If the authorization is "Basic" (username+password) it tries
to authenticate the user
"""
auth_type, value, *_ = request.META.get("HTTP_AUTHORIZATION").split()
if not self.has_auth_header(request):
return None

auth_type, value, *_ = request.META.get(AUTH_HEADER).split()

if auth_type == "Basic":
decoded_username, decoded_password = (
Expand All @@ -37,10 +42,10 @@ def get_token_from_request(self, request) -> Optional[Token]:
token = Token.from_credentials(decoded_username, decoded_password)
if token:
# Convert the request "Basic" auth to "Bearer" with access token
request.META["HTTP_AUTHORIZATION"] = f"Bearer {token.access_token}"
request.META[AUTH_HEADER] = f"Bearer {token.access_token}"
else:
# Setup an invalid dummy bearer token
request.META["HTTP_AUTHORIZATION"] = "Bearer not-valid-token"
request.META[AUTH_HEADER] = "Bearer not-valid-token"

elif auth_type == "Bearer":
token = Token.from_access_token(value)
Expand Down Expand Up @@ -95,7 +100,7 @@ def append_user_info_to_request(self, request, token: Token):
@staticmethod
def has_auth_header(request) -> bool:
"""Check if exists an authentication header in the HTTP request"""
return "HTTP_AUTHORIZATION" in request.META
return AUTH_HEADER in request.META

def process_request(self, request):
"""
Expand Down
5 changes: 4 additions & 1 deletion src/django_keycloak/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ def from_credentials(cls, username: str, password: str) -> Optional[Token]: # t
# and post error (account not completed.)
except (KeycloakAuthenticationError, KeycloakPostError) as err:
logger.debug(
f"{type(err).__name__}: {err.args}", exc_info=settings.TRACE_DEBUG_LOGS
"%s: %s",
type(err).__name__,
err.args,
exc_info=settings.TRACE_DEBUG_LOGS,
)
return None

Expand Down

0 comments on commit 6a8af99

Please sign in to comment.