Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions api/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from django.views.decorators.http import require_GET
from django.http import HttpResponseRedirect, HttpResponse
from furl import furl
from website import settings
from framework.auth import cas
from framework.auth.utils import print_cas_log, LogLevel

def make_response_from_ticket(ticket, service_url):
"""
Given a CAS ticket and service URL, attempt to validate the user and return user object.

:param str ticket: CAS service ticket
:param str service_url: Service URL from which the authentication request originates
:return: user object if authentication is successful, otherwise an HttpResponse with an error message and status code
"""

service_furl = furl(service_url)
if 'ticket' in service_furl.args:
service_furl.remove(args=['ticket'])
client = cas.get_client()
cas_resp = client.service_validate(ticket, service_furl.url)
if cas_resp.authenticated:
user, external_credential, action = cas.get_user_from_cas_resp(cas_resp)
if user and action == 'authenticate':
print_cas_log(
f'CAS response - authenticating user: user=[{user._id}], '
f'external=[{external_credential}], action=[{action}]',
LogLevel.INFO,
)
# if user is authenticated by CAS
print_cas_log(f'CAS response - finalizing authentication: user=[{user._id}]', LogLevel.INFO)
return user

return HttpResponse('CAS authentication failed', status=401)

@require_GET
def auth_login(request):
ticket = request.GET.get('ticket')
if not ticket:
return HttpResponse('Missing ticket', status=400)

# redirect to Angular
next_url = request.GET.get('next', 'http://localhost:4200/')
response = HttpResponseRedirect(next_url)

from osf.utils.fields import ensure_str
from django.contrib.auth import login
import itsdangerous

service_url = furl(request.build_absolute_uri()).remove(args=['ticket'])
user_or_response = make_response_from_ticket(ticket, service_url.url)
if isinstance(user_or_response, HttpResponse):
return user_or_response
login(request, user_or_response, backend='api.base.authentication.backends.ODMBackend')
session = request.session
data = {
'auth_user_username': user_or_response.username,
'auth_user_id': user_or_response._primary_key,
'auth_user_fullname': user_or_response.fullname,
'user_reference_uri': user_or_response.get_semantic_iri(),
}
for key, value in data.items() if data else {}:
session[key] = value

# Note: session.modified is set to False here to prevent Django from saving the session again in SessionMiddleware.process_response,
# which would overwrite the session cookie set here with an unsigned version.
# Setting cookie can be done in process_response by adding session_key signing.
session.modified = False
session.save()

session_key = session._get_or_create_session_key()
signed_session_key = ensure_str(itsdangerous.Signer(settings.SECRET_KEY).sign(session_key))

response.set_cookie(
'osf',
signed_session_key,
domain=settings.OSF_COOKIE_DOMAIN,
secure=settings.SESSION_COOKIE_SECURE,
httponly=settings.SESSION_COOKIE_HTTPONLY,
samesite=settings.SESSION_COOKIE_SAMESITE,
)
from django.middleware.csrf import get_token

csrf_token = get_token(request)

response.set_cookie(
'api-csrf',
csrf_token,
samesite='Lax',
)

return response
73 changes: 73 additions & 0 deletions api/base/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ def process_response(self, request, response):
return response


# import time
# from importlib import import_module

# from django.contrib.sessions.backends.base import UpdateError
# from django.contrib.sessions.exceptions import SessionInterrupted
# from django.utils.cache import patch_vary_headers
# from django.utils.deprecation import MiddlewareMixin
# from django.utils.http import http_date


class UnsignCookieSessionMiddleware(SessionMiddleware):
"""
Overrides the process_request hook of SessionMiddleware
Expand All @@ -132,3 +142,66 @@ def process_request(self, request):
request.session = drf_get_session_from_cookie(cookie)
else:
request.session = SessionStore()

# Example of process_response with session cookie signing. Not used currently as signing is done in auth_login view and session cookie is set there.
# def process_response(self, request, response):
# """
# If request.session was modified, or if the configuration is to save the
# session every time, save the changes and set a session cookie or delete
# the session cookie if the session has been emptied.
# """
# try:
# accessed = request.session.accessed
# modified = request.session.modified
# empty = request.session.is_empty()
# except AttributeError:
# return response
# # First check if we need to delete this cookie.
# # The session should be deleted only if the session is entirely empty.
# if settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
# response.delete_cookie(
# settings.SESSION_COOKIE_NAME,
# path=settings.SESSION_COOKIE_PATH,
# domain=settings.SESSION_COOKIE_DOMAIN,
# samesite=settings.SESSION_COOKIE_SAMESITE,
# )
# patch_vary_headers(response, ("Cookie",))
# else:
# if accessed:
# patch_vary_headers(response, ("Cookie",))
# if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
# if request.session.get_expire_at_browser_close():
# max_age = None
# expires = None
# else:
# max_age = request.session.get_expiry_age()
# expires_time = time.time() + max_age
# expires = http_date(expires_time)
# # Save the session data and refresh the client cookie.
# # Skip session save for 5xx responses.
# if response.status_code < 500:
# try:
# request.session.save()
# except UpdateError:
# raise SessionInterrupted(
# "The request's session was deleted before the "
# "request completed. The user may have logged "
# "out in a concurrent request, for example."
# )

# from osf.utils.fields import ensure_str
# import itsdangerous

# signed_session_key = ensure_str(itsdangerous.Signer(settings.SECRET_KEY).sign(request.session.session_key))
# response.set_cookie(
# settings.SESSION_COOKIE_NAME,
# signed_session_key,
# max_age=max_age,
# expires=expires,
# domain=settings.SESSION_COOKIE_DOMAIN,
# path=settings.SESSION_COOKIE_PATH,
# secure=settings.SESSION_COOKIE_SECURE or None,
# httponly=settings.SESSION_COOKIE_HTTPONLY or None,
# samesite=settings.SESSION_COOKIE_SAMESITE,
# )
# return response
4 changes: 3 additions & 1 deletion api/base/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from django.urls import include, re_path
from django.urls import include, re_path, path
from django.views.generic.base import RedirectView


from api.base import views
from api.base import settings
from api.base import versioning
from api.providers.views import RegistrationBulkCreate
from api.auth import auth_login

default_version = versioning.decimal_version_to_url_path(settings.REST_FRAMEWORK['DEFAULT_VERSION'])

Expand Down Expand Up @@ -86,6 +87,7 @@
),
),
re_path(r'^$', RedirectView.as_view(pattern_name=views.root), name='redirect-to-root', kwargs={'version': default_version}),
path('login', auth_login, name='login'),
]

# Add django-silk URLs if it's in INSTALLED_APPS
Expand Down