Skip to content
Closed
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
15 changes: 3 additions & 12 deletions .github/workflows/django.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,8 @@ jobs:
max-parallel: 4
matrix:
include:
- python-version: 3.7
django-version: Django==3.1

- python-version: 3.7
django-version: Django==3.2

- python-version: 3.8
django-version: Django==3.1

- python-version: 3.8
django-version: Django==3.2
- python-version: 3.11
django-version: Django==4.2

steps:
- uses: actions/checkout@v2
Expand All @@ -36,7 +27,7 @@ jobs:
python -m pip install --upgrade pip
pip install ${{ matrix.django-version }}
pip install -r example/requirements.txt
pip install -e .[msdal]
pip install -e .[mssso]
- name: Flake
run: |
flake8 --config .config/flake8 --ignore E1,E23,W503 auth_token
Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Build and publish package to PyPI

on: push

jobs:
build-n-publish:
name: Build and publish package to PyPI
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@master
- name: Set up Python 3.11
uses: actions/setup-python@v3
with:
python-version: "3.11"
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish package to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
verify_metadata: false
6 changes: 4 additions & 2 deletions auth_token/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@
'EXPIRATION_DELTA': 0, # Authorization token expiration will be extended, only if original expiration is at
# least "X" seconds older, than new one (default: 0 seconds, i.e. always extend)
'TAKEOVER_ENABLED': True, # Turns on/off takeover functionality
'MS_SSO_APP_ID': None, # Set AppID for MS SSO authentication
'MS_SSO_TENANT_ID': None, # Set TentnatID for MS SSO authentication
'MS_SSO_PROTOCOL': None, # Protocol of SSO (possible values are "oauth" or "saml")
'MS_SSO_APP_ID': None, # Set AppID for MS SSO authentication (OAuth only)
'MS_SSO_TENANT_ID': None, # Set TentnatID for MS SSO authentication (OAuth only)
'MS_SSO_METADATA_URL': None, # Set Metadata URL for MS SSO authentication (SAML only)
}


Expand Down
10 changes: 4 additions & 6 deletions auth_token/contrib/common/default/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from urllib.parse import quote_plus

from django.contrib.auth.views import LoginView, LogoutView
from django.utils.decorators import method_decorator
from django.urls import reverse, NoReverseMatch
Expand Down Expand Up @@ -40,7 +42,7 @@ def _get_sso_login_methods(self):
return [
{
'name': 'microsoft',
'url': f'{reverse("ms-sso-login")}?next={self.request.GET.get("next", "/")}',
'url': f'{reverse("ms-sso-login")}?next={quote_plus(self.request.GET.get("next", "/"), safe="/")}',
'label': gettext('Continue with Microsoft account')
}
]
Expand All @@ -58,11 +60,7 @@ class TokenLogoutView(LogoutView):
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
logout(request)
next_page = self.get_next_page()
if next_page:
# Redirect to this page until the session has been cleared.
return HttpResponseRedirect(next_page)
return super(LogoutView, self).dispatch(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)


class InputLogMixin:
Expand Down
4 changes: 4 additions & 0 deletions auth_token/contrib/is_core_auth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
from django.views.generic.base import RedirectView
from django.urls import reverse

from security.decorators import throttling_all

import import_string
from auth_token.config import settings
from auth_token.contrib.common.auth_security.validators import LOGIN_THROTTLING_VALIDATORS
from auth_token.contrib.common.views import LoginView as _LoginView
from auth_token.contrib.common.views import LogoutView as _LogoutView
from auth_token.contrib.common.views import LoginCodeVerificationView as _LoginCodeVerificationView
Expand Down Expand Up @@ -85,6 +88,7 @@ def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)


@throttling_all(*LOGIN_THROTTLING_VALIDATORS)
class LoginCodeVerificationView(_LoginCodeVerificationView):

template_name = 'is_core/login.html'
Expand Down
53 changes: 44 additions & 9 deletions auth_token/contrib/ms_sso/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,64 @@

UserModel = get_user_model()


class MsSsoBackend(ModelBackend):
class BaseMsSsoBackend(ModelBackend):
"""
Authenticates device with MS SSO
"""

def _get_user_from_ms_user_data(self, ms_user_data):
username = ms_user_data['userPrincipalName']
def _get_natural_key(self):
raise NotImplementedError

def _get_user_from_natural_key(self, natural_key):
try:
return UserModel._default_manager.get_by_natural_key(username)
return UserModel._default_manager.det_by_natural_key(self._get_natural_key())
except UserModel.DoesNotExist:
return None

def authenticate(self, request, mso_token=None, **kwargs):
def authenticate(self, request, **kwargs):
if not mso_token:
return None

ms_user_data = get_user_data(mso_token)
if not ms_user_data:
self.ms_user_data = get_user_data(mso_token)
if not self.ms_user_data:
return None

user = self._get_user_from_ms_user_data(ms_user_data)
user = self._get_user_from_natural_key()
if user and self.user_can_authenticate(user):
return user
else:
return None

class MsSsoOauthBackend(ModelBackend):
"""
Authenticates device with MS SSO
"""

def _get_natural_key(self):
username = self.ms_user_data['userPrincipalName']
try:
return UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
return None

def authenticate(self, request, mso_token=None, **kwargs):
if not mso_token:
return None

self.ms_user_data = get_user_data(mso_token)
if not self.ms_user_data:
return None

return super().authenticate(request)


class MsSsoSamlBackend(ModelBackend):
"""
Authenticates device with MS SSO
"""

def _get_natural_key(self):
return "ABC"

def authenticate(self, request, mso_token=None, **kwargs):
self.request = request
26 changes: 23 additions & 3 deletions auth_token/contrib/ms_sso/urls.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
from django.urls import path

from .views import MsCallback, MsLogin
from .views import MsOauthLogin, MsSamlLogin, MsSamlCallback, MsOauthCallback
from auth_token.config import settings


def _get_view(key):
VIEWS = {
"oauth": {
"login": MsOauthLogin,
"callback": MsOauthCallback,
},
"saml": {
"login": MsSamlLogin,
"callback": MsSamlCallback,
}
}
protocol = settings.MS_SSO_PROTOCOL
if settings.MS_SSO_PROTOCOL not in VIEWS.keys():
raise ValueError("MS SSO Protocol \"{protocol}\" is not supported.")

return VIEWS[protocol][key]



urlpatterns = [
path(
'login/mso',
MsLogin.as_view(),
_get_view("login").as_view(),
name='ms-sso-login',
),
path(
'login/mso/callback',
MsCallback.as_view(),
_get_view("callback").as_view(),
name='ms-sso-redirect',
),
]
50 changes: 34 additions & 16 deletions auth_token/contrib/ms_sso/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .helpers import get_sign_in_flow, acquire_token_by_auth_code_flow


class MsLogin(RedirectView):
class MsOauthLogin(RedirectView):

def get_redirect_url(self, *args, **kwargs):
sign_flow = get_sign_in_flow()
Expand All @@ -23,26 +23,19 @@ def get_redirect_url(self, *args, **kwargs):
return sign_flow['auth_uri']


class MsCallback(RedirectView):
class MsSamlLogin(RedirectView):

allowed_cookie = True
allowed_header = False
def get_redirect_url(self, *args, **kwargs):
return "http://placeholder"

def get(self, *args, **kwargs):
if not hasattr(self.request, 'session'):
raise ImproperlyConfigured('Django SessionMiddleware must be enabled to use MS SSO')

sign_flow = self.request.session.get('auth_token_ms_sso_auth_flow')
if not sign_flow:
messages.error(self.request, gettext('Microsoft SSO login was unsuccessful, please try it again'))
return redirect_to_login('')
class BaseMsCallback(RedirectView):

result = acquire_token_by_auth_code_flow(sign_flow, self.request.GET)
if 'access_token' not in result:
messages.error(self.request, gettext('Microsoft SSO login was unsuccessful, please try it again'))
return redirect_to_login(sign_flow['next'])
allowed_cookie = True
allowed_header = False

user = authenticate(mso_token=result['access_token'])
def _do_login(self, **kwargs):
user = authenticate(**kwargs)
if not user:
messages.error(
self.request, gettext('Microsoft SSO login was unsuccessful, please use another login method')
Expand All @@ -57,3 +50,28 @@ def get(self, *args, **kwargs):
two_factor_login=False
)
return HttpResponseRedirect(sign_flow['next'])


class MsOauthCallback(BaseMsCallback):

def get(self, *args, **kwargs):
if not hasattr(self.request, 'session'):
raise ImproperlyConfigured('Django SessionMiddleware must be enabled to use MS SSO')

sign_flow = self.request.session.get('auth_token_ms_sso_auth_flow')
if not sign_flow:
messages.error(self.request, gettext('Microsoft SSO login was unsuccessful, please try it again'))
return redirect_to_login('')

result = acquire_token_by_auth_code_flow(sign_flow, self.request.GET)
if 'access_token' not in result:
messages.error(self.request, gettext('Microsoft SSO login was unsuccessful, please try it again'))
return redirect_to_login(sign_flow['next'])

return _do_login(mso_token=result['access_token'])


class MsSamlCallback(BaseMsCallback):

def post(self, *args, **kwargs):
return self._do_login(saml_callback_request=self.request)
5 changes: 4 additions & 1 deletion auth_token/contrib/rest_framework_auth/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ def enforce_csrf(self, request):
"""
Enforce CSRF validation for session based authentication.
"""
reason = CSRFCheck().process_view(request, None, (), {})

def dummy_get_response(_): # pragma: no cover
return None
reason = CSRFCheck(dummy_get_response).process_view(request, None, (), {})
if reason:
# CSRF failed, bail with explicit error message
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
Expand Down
4 changes: 2 additions & 2 deletions auth_token/middleware.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import timedelta
import time

from django.utils.encoding import force_text
from django.utils.encoding import force_str
from django.utils.functional import SimpleLazyObject
from django.utils.http import http_date

Expand Down Expand Up @@ -47,7 +47,7 @@ def _update_token_and_cookie(self, request, response, max_age, expires):
if expires_at - request.token.expires_at >= timedelta(seconds=settings.EXPIRATION_DELTA):
request.token.change_and_save(expires_at=expires_at, update_only_changed_fields=True)
if settings.COOKIE and request.token.allowed_cookie:
response.set_cookie(settings.COOKIE_NAME, force_text(request.token.secret_key), max_age=max_age,
response.set_cookie(settings.COOKIE_NAME, force_str(request.token.secret_key), max_age=max_age,
expires=expires, httponly=settings.COOKIE_HTTPONLY,
secure=settings.COOKIE_SECURE, domain=settings.COOKIE_DOMAIN)
return response
Expand Down
5 changes: 5 additions & 0 deletions auth_token/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,11 @@ def _pre_save(self, changed, changed_fields, *args, **kwargs):
is_primary=False
)

def _post_save(self, changed, changed_fields, *args, **kwargs):
super()._post_save(changed, changed_fields, *args, **kwargs)
if 'is_active' in changed_fields and not self.is_active:
self.authorization_tokens.update(is_active=False)

def set_login_token(self, raw_token):
self.login_token = make_password(raw_token)

Expand Down
6 changes: 3 additions & 3 deletions auth_token/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import import_string

from django.core.exceptions import ImproperlyConfigured
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.conf import settings as django_settings
from django.contrib.auth import _get_backends, load_backend
from django.contrib.auth.models import AnonymousUser
Expand Down Expand Up @@ -86,7 +86,7 @@ def hash_key(key, salt=None):
settings.HASH_SALT,
key,
secret='|'.join((salt or '', django_settings.SECRET_KEY)),
algorithm=django_settings.DEFAULT_HASHING_ALGORITHM,
algorithm="sha256",
).hexdigest()


Expand Down Expand Up @@ -273,7 +273,7 @@ def parse_auth_header_value(request):
header_value = request.META.get(header_name_to_django(settings.HEADER_NAME))

if not header_value:
raise ValueError('Authorization header missing')
raise PermissionDenied("Authorization header missing")

if settings.HEADER_TOKEN_TYPE is None:
return header_value
Expand Down
2 changes: 1 addition & 1 deletion auth_token/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION = (0, 2, 15)
VERSION = (0, 4, 0)


def get_version():
Expand Down
2 changes: 1 addition & 1 deletion example/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ cleanall: cleanvar

pip:
$(PYTHON_BIN)/pip install -r requirements.txt
$(PYTHON_BIN)/pip install -e ../.[msdal]
$(PYTHON_BIN)/pip install -e ../.[mssso]

initvirtualenv:
virtualenv -p $(PYTHON) $(VIRTUAL_ENV)
Expand Down
Loading
Loading