Skip to content

Commit 94aa6a7

Browse files
authored
Merge pull request #241 from City-of-Helsinki/KK-731-improve-authentication
KK-731 | Improve authentication implementation
2 parents 61f26bc + 418050d commit 94aa6a7

18 files changed

Lines changed: 193 additions & 83 deletions

File tree

children/schema.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@
1313
from graphene_django import DjangoConnectionField
1414
from graphene_django.filter import DjangoFilterConnectionField
1515
from graphene_django.types import DjangoObjectType
16-
from graphql_jwt.decorators import login_required
1716
from graphql_relay import from_global_id
1817
from graphql_relay.connection.arrayconnection import offset_to_cursor
1918

2019
from children.notifications import NotificationType
2120
from common.schema import set_obj_languages_spoken_at_home
22-
from common.utils import update_object
21+
from common.utils import login_required, update_object
2322
from kukkuu.exceptions import (
2423
ApiUsageError,
2524
DataValidationError,

common/utils.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import binascii
22
from copy import deepcopy
3+
from functools import wraps
34

5+
from django.core.exceptions import PermissionDenied
46
from django.db import transaction
57
from graphene import Node
6-
from graphql_jwt.decorators import user_passes_test
7-
from graphql_jwt.exceptions import PermissionDenied
8+
from graphql.execution.base import ResolveInfo
89
from graphql_relay import from_global_id, to_global_id
910

1011
from kukkuu import __version__
@@ -78,6 +79,34 @@ def get_obj_if_user_can_administer(info, global_id, expected_obj_type):
7879
return obj
7980

8081

82+
def context(f):
83+
def decorator(func):
84+
def wrapper(*args, **kwargs):
85+
info = next(arg for arg in args if isinstance(arg, ResolveInfo))
86+
return func(info.context, *args, **kwargs)
87+
88+
return wrapper
89+
90+
return decorator
91+
92+
93+
def user_passes_test(test_func):
94+
def decorator(f):
95+
@wraps(f)
96+
@context(f)
97+
def wrapper(context, *args, **kwargs):
98+
if test_func(context.user):
99+
return f(*args, **kwargs)
100+
raise PermissionDenied("You do not have permission to perform this action")
101+
102+
return wrapper
103+
104+
return decorator
105+
106+
107+
# copied from https://github.com/flavors/django-graphql-jwt/blob/704f24e7ebbea0b81015ef3c1f4a302e9d432ecf/graphql_jwt/decorators.py # noqa
108+
login_required = user_passes_test(lambda u: u.is_authenticated)
109+
81110
project_user_required = user_passes_test(
82111
lambda u: u.is_authenticated and u.administered_projects
83112
)

events/schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from graphene_django import DjangoObjectType
1313
from graphene_django.filter import DjangoFilterConnectionField
1414
from graphene_file_upload.scalars import Upload
15-
from graphql_jwt.decorators import login_required
1615
from graphql_relay import from_global_id
1716

1817
from children.models import Child
@@ -21,6 +20,7 @@
2120
from common.utils import (
2221
get_node_id_from_global_id,
2322
get_obj_if_user_can_administer,
23+
login_required,
2424
project_user_required,
2525
update_object,
2626
update_object_with_translations,

kukkuu/consts.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@
2626
SINGLE_EVENTS_DISALLOWED_ERROR = "SINGLE_EVENTS_DISALLOWED_ERROR"
2727
TICKET_SYSTEM_URL_MISSING_ERROR = "TICKET_SYSTEM_URL_MISSING_ERROR"
2828
NO_FREE_TICKET_SYSTEM_PASSWORDS_ERROR = "NO_FREE_TICKET_SYSTEM_PASSWORDS_ERROR"
29+
UNAUTHENTICATED_ERROR = "UNAUTHENTICATED_ERROR"
30+
AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR"
31+
AUTHENTICATION_EXPIRED_ERROR = "AUTHENTICATION_EXPIRED_ERROR"

kukkuu/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,7 @@ class TicketSystemUrlMissingError(KukkuuGraphQLError):
9191

9292
class NoFreeTicketSystemPasswordsError(KukkuuGraphQLError):
9393
"""A free ticket system password is needed but there isn't any."""
94+
95+
96+
class AuthenticationExpiredError(KukkuuGraphQLError):
97+
"""Authentication expired."""

kukkuu/graphene.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from helusers.oidc import AuthenticationError
2+
from jose import ExpiredSignatureError
3+
4+
from kukkuu.exceptions import AuthenticationExpiredError
5+
6+
7+
# pretty much copied from https://github.com/City-of-Helsinki/open-city-profile/blob/4f46f9f9f195c4254f79f5dfbd97d03b7fa87a5b/open_city_profile/graphene.py#L18 # noqa
8+
class JWTMiddleware:
9+
def resolve(self, next, root, info, **kwargs):
10+
request = info.context
11+
12+
auth_error = getattr(request, "auth_error", None)
13+
if isinstance(auth_error, Exception):
14+
# TODO with the current version of django-helusers (v0.7.0) there is no
15+
# proper way to catch only expired token errors, so this kind of hax is
16+
# needed for that. If/when helusers offers a way to do this properly
17+
# this implementation should be changed.
18+
if isinstance(auth_error, AuthenticationError) and isinstance(
19+
auth_error.__context__, ExpiredSignatureError
20+
):
21+
raise AuthenticationExpiredError(
22+
"Invalid Authorization header. JWT has expired."
23+
) from auth_error
24+
raise auth_error
25+
26+
return next(root, info, **kwargs)

kukkuu/middleware.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from helusers.oidc import RequestJWTAuthentication
2+
3+
4+
# copied from https://github.com/City-of-Helsinki/open-city-profile/blob/4f46f9f9f195c4254f79f5dfbd97d03b7fa87a5b/open_city_profile/middleware.py#L6 # noqa
5+
class JWTAuthentication:
6+
def __init__(self, get_response):
7+
self.get_response = get_response
8+
9+
def __call__(self, request):
10+
if not request.user.is_authenticated:
11+
try:
12+
authenticator = RequestJWTAuthentication()
13+
user_auth = authenticator.authenticate(request)
14+
if user_auth is not None:
15+
request.user_auth = user_auth
16+
request.user = user_auth.user
17+
except Exception as e:
18+
request.auth_error = e
19+
20+
return self.get_response(request)

kukkuu/oidc.py

Lines changed: 0 additions & 36 deletions
This file was deleted.

kukkuu/settings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@
174174
"django.contrib.auth.middleware.AuthenticationMiddleware",
175175
"django.contrib.messages.middleware.MessageMiddleware",
176176
"django.middleware.clickjacking.XFrameOptionsMiddleware",
177+
"kukkuu.middleware.JWTAuthentication",
177178
]
178179

179180
TEMPLATES = [
@@ -199,7 +200,6 @@
199200

200201
AUTHENTICATION_BACKENDS = [
201202
"django.contrib.auth.backends.ModelBackend",
202-
"kukkuu.oidc.GraphQLApiTokenAuthentication",
203203
"guardian.backends.ObjectPermissionBackend",
204204
]
205205

@@ -226,7 +226,7 @@
226226

227227
GRAPHENE = {
228228
"SCHEMA": "kukkuu.schema.schema",
229-
"MIDDLEWARE": ["graphql_jwt.middleware.JSONWebTokenMiddleware"],
229+
"MIDDLEWARE": ["kukkuu.graphene.JWTMiddleware"],
230230
}
231231

232232
GRAPHQL_JWT = {"JWT_AUTH_HEADER_PREFIX": "Bearer"}

kukkuu/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)