Skip to content

Commit 27ef96c

Browse files
authored
Merge pull request #166 from Seykotron/master
Setting a second USERNAME_CLAIM to allow guest users to log in
2 parents 90317a1 + 84c3a41 commit 27ef96c

File tree

9 files changed

+109
-9
lines changed

9 files changed

+109
-9
lines changed

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Changelog
22
=========
33

4+
`1.9.0`_ - 2021-08-27
5+
---------------------
6+
7+
**Features**
8+
9+
* Add ``GUEST_USERNAME_CLAIM``, a setting that allow you to use a different username claim for guest users. @JonasKs and @Seykotron #166
10+
11+
412
`1.8.1`_ - 2021-08-27
513
---------------------
614

@@ -261,6 +269,7 @@ Changelog
261269

262270
* Initial release
263271

272+
.. _1.9.0: https://github.com/snok/django-auth-adfs/compare/1.8.1...1.9.0
264273
.. _1.8.1: https://github.com/snok/django-auth-adfs/compare/1.8.0...1.8.1
265274
.. _1.8.0: https://github.com/snok/django-auth-adfs/compare/1.7.0...1.8.0
266275
.. _1.7.0: https://github.com/snok/django-auth-adfs/compare/1.6.1...1.7.0

django_auth_adfs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
Adding imports here will break setup.py
55
"""
66

7-
__version__ = '1.8.1'
7+
__version__ = '1.9.0'

django_auth_adfs/backend.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,22 @@ def create_user(self, claims):
128128
"""
129129
# Create the user
130130
username_claim = settings.USERNAME_CLAIM
131+
guest_username_claim = settings.GUEST_USERNAME_CLAIM
131132
usermodel = get_user_model()
132133

134+
if (
135+
guest_username_claim
136+
and not claims.get(username_claim)
137+
and not settings.BLOCK_GUEST_USERS
138+
and claims.get('tid') != settings.TENANT_ID
139+
):
140+
username_claim = guest_username_claim
141+
133142
if not claims.get(username_claim):
134-
logger.error("User claim's doesn't have the claim '%s' in his claims: %s" % (username_claim, claims))
143+
logger.error("User claim's doesn't have the claim '%s' in his claims: %s" %
144+
(username_claim, claims))
135145
raise PermissionDenied
146+
136147
userdata = {usermodel.USERNAME_FIELD: claims[username_claim]}
137148

138149
try:

django_auth_adfs/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def __init__(self):
7070
self.TENANT_ID = None # Required
7171
self.TIMEOUT = 5
7272
self.USERNAME_CLAIM = "winaccountname"
73+
self.GUEST_USERNAME_CLAIM = None
7374
self.JWT_LEEWAY = 0
7475
self.CUSTOM_FAILED_RESPONSE_VIEW = lambda request, error_message, status: render(
7576
request, 'django_auth_adfs/login_failed.html', {'error_message': error_message}, status=status

docs/settings_ref.rst

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,25 @@ example
262262
The group doesn't need to exist in Django for this to work. This will work as long as it's in the groups claim
263263
in the access token.
264264

265+
GUEST_USERNAME_CLAIM
266+
--------------------
267+
* **Default**: ``None``
268+
* **Type**: ``string``
269+
270+
When these criteria are met:
271+
272+
1. A ``guest_username_claim`` is configured
273+
2. Token claims do not have the configured ``settings.USERNAME_CLAIM`` in it
274+
3. The ``settings.BLOCK_GUEST_USERS`` is set to ``False``
275+
4. The claims ``tid`` does not match ``settings.TENANT_ID``
276+
277+
Then, the ``GUEST_USERNAME_CLAIM`` can be used to populate a username, when the ``USERNAME_CLAIM`` cannot be found in
278+
the claims.
279+
280+
This can be useful when you want to use ``upn`` as a username claim for your own users,
281+
but some guest users (such as normal outlook users) don't have that claim.
282+
283+
265284
LOGIN_EXEMPT_URLS
266285
-----------------
267286
* **Default**: ``None``
@@ -423,13 +442,13 @@ The value of the claim must be a unique value. No 2 users should ever have the s
423442
.. NOTE::
424443
You can find the short name for the claims you configure in the ADFS management console underneath
425444
**ADFS** ➜ **Service** ➜ **Claim Descriptions**
426-
427-
445+
446+
428447
.. _version_setting:
429448

430449
VERSION
431450
--------------
432-
* **Default**: ``v1.0``
451+
* **Default**: ``v1.0``
433452
* **Type**: ``string``
434453

435454
Version of the Azure Active Directory endpoint version. By default it is set to ``v1.0``. At the time of writing this documentation, it can also be set to ``v2.0``. For new projects, ``v2.0`` is recommended. ``v1.0`` is kept as a default for backwards compatibility.

poetry.lock

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = 'django-auth-adfs'
3-
version = '1.8.1' # Remember to also change __init__.py version
3+
version = '1.9.0' # Remember to also change __init__.py version
44
description = 'A Django authentication backend for Microsoft ADFS and AzureAD'
55
authors = ['Joris Beckers <[email protected]>']
66
maintainers = ['Jonas Krüger Svensson <[email protected]>', 'Sondre Lillebø Gundersen <[email protected]>']
@@ -46,6 +46,7 @@ responses = '*'
4646
mock = '*'
4747
coverage = '*'
4848
djangorestframework = '*'
49+
django-filter = "^2.4.0"
4950

5051
[build-system]
5152
requires = ["poetry-core>=1.0.0"]

tests/test_drf_integration.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from django_auth_adfs.config import ProviderConfig, Settings
1010
from django_auth_adfs.rest_framework import AdfsAccessTokenAuthentication
1111
from .utils import build_access_token_adfs, build_access_token_azure, build_access_token_azure_guest, \
12-
build_access_token_azure_not_guest, mock_adfs
12+
build_access_token_azure_guest_no_upn, build_access_token_azure_not_guest, mock_adfs
1313

1414

1515
class RestFrameworkIntegrationTests(TestCase):
@@ -28,6 +28,9 @@ def setUp(self):
2828
azure_response_no_guest = build_access_token_azure_not_guest(RequestFactory().get('/'))[2]
2929
self.access_token_azure_no_guest = json.loads(azure_response_no_guest)['access_token']
3030

31+
azure_response_guest = build_access_token_azure_guest_no_upn(RequestFactory().get('/'))[2]
32+
self.access_token_azure_guest_no_upn = json.loads(azure_response_guest)['access_token']
33+
3134
@mock_adfs("2012")
3235
def test_access_token_2012(self):
3336
access_token_header = "Bearer {}".format(self.access_token_adfs)
@@ -95,6 +98,40 @@ def test_access_token_azure_no_guest(self):
9598
user, token = self.drf_auth_class.authenticate(request)
9699
self.assertEqual(user.username, "testuser")
97100

101+
@mock_adfs("azure")
102+
def test_access_token_azure_guest_but_no_upn(self):
103+
access_token_header = "Bearer {}".format(self.access_token_azure_guest_no_upn)
104+
request = RequestFactory().get('/api', HTTP_AUTHORIZATION=access_token_header)
105+
from django_auth_adfs.config import django_settings
106+
settings = deepcopy(django_settings)
107+
del settings.AUTH_ADFS["SERVER"]
108+
settings.AUTH_ADFS["TENANT_ID"] = "dummy_tenant_id"
109+
settings.AUTH_ADFS["GUEST_USERNAME_CLAIM"] = "email"
110+
settings.AUTH_ADFS["BLOCK_GUEST_USERS"] = False
111+
with patch("django_auth_adfs.config.django_settings", settings):
112+
with patch('django_auth_adfs.backend.settings', Settings()):
113+
with patch("django_auth_adfs.config.settings", Settings()):
114+
with patch("django_auth_adfs.backend.provider_config", ProviderConfig()):
115+
user, token = self.drf_auth_class.authenticate(request)
116+
self.assertEqual(user.username, "[email protected]")
117+
118+
@mock_adfs("azure")
119+
def test_access_token_azure_guest_but_no_upn_but_no_guest_username_claim(self):
120+
access_token_header = "Bearer {}".format(self.access_token_azure_guest_no_upn)
121+
request = RequestFactory().get('/api', HTTP_AUTHORIZATION=access_token_header)
122+
from django_auth_adfs.config import django_settings
123+
settings = deepcopy(django_settings)
124+
del settings.AUTH_ADFS["SERVER"]
125+
settings.AUTH_ADFS["TENANT_ID"] = "dummy_tenant_id"
126+
settings.AUTH_ADFS["GUEST_USERNAME_CLAIM"] = None # <--- Set to None, should not be validated as OK
127+
settings.AUTH_ADFS["BLOCK_GUEST_USERS"] = False
128+
with patch("django_auth_adfs.config.django_settings", settings):
129+
with patch('django_auth_adfs.backend.settings', Settings()):
130+
with patch("django_auth_adfs.config.settings", Settings()):
131+
with patch("django_auth_adfs.backend.provider_config", ProviderConfig()):
132+
with self.assertRaises(exceptions.AuthenticationFailed):
133+
self.drf_auth_class.authenticate(request)
134+
98135
@mock_adfs("2012")
99136
def test_access_token_exceptions(self):
100137
access_token_header = "Bearer non-existing-token"

tests/utils.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,17 @@ def build_access_token_azure_guest(request):
8383
return do_build_access_token(request, issuer, schema='guest_tenant_id')
8484

8585

86+
def build_access_token_azure_guest_no_upn(request):
87+
issuer = "https://sts.windows.net/01234567-89ab-cdef-0123-456789abcdef/"
88+
return do_build_access_token(request, issuer, schema='guest_tenant_id', no_upn=True)
89+
90+
8691
def do_build_mfa_error(request):
8792
response = {'error_description': 'AADSTS50076'}
8893
return 400, [], json.dumps(response)
8994

9095

91-
def do_build_access_token(request, issuer, schema=None):
96+
def do_build_access_token(request, issuer, schema=None, no_upn=False):
9297
issued_at = int(time.time())
9398
expires = issued_at + 3600
9499
auth_time = datetime.utcnow()
@@ -116,6 +121,8 @@ def do_build_access_token(request, issuer, schema=None):
116121
if issuer.startswith('https://sts.windows.net'):
117122
claims['upn'] = 'testuser'
118123
claims['groups'] = claims['group']
124+
if no_upn:
125+
del claims['upn']
119126
token = jwt.encode(claims, signing_key_b, algorithm="RS256")
120127
response = {
121128
'resource': 'django_website.adfs.relying_party_id',

0 commit comments

Comments
 (0)