From 92f72eb7fee585813e1c404faa896a40b0ecef02 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Tue, 11 Jan 2022 09:23:22 +0100
Subject: [PATCH] auth_oidc: add a sub-to-email matching strategy (#60488)

---
 src/authentic2_auth_oidc/backends.py          |  7 ++++
 .../migrations/0004_auto_20171017_1522.py     |  1 +
 src/authentic2_auth_oidc/models.py            |  2 +
 tests/test_auth_oidc.py                       | 39 +++++++++++++++++++
 4 files changed, 49 insertions(+)

diff --git a/src/authentic2_auth_oidc/backends.py b/src/authentic2_auth_oidc/backends.py
index 6c75b321..eb3c71aa 100644
--- a/src/authentic2_auth_oidc/backends.py
+++ b/src/authentic2_auth_oidc/backends.py
@@ -149,6 +149,13 @@ class OIDCBackend(ModelBackend):
             else:
                 user = users[0]
                 logger.info('auth_oidc: found user using username (=sub) "%s": %s', id_token.sub, user)
+        elif provider.strategy == models.OIDCProvider.STRATEGY_FIND_EMAIL:
+            users = User.objects.filter(email=id_token.sub, is_active=True).order_by('pk')
+            if not users:
+                logger.warning('auth_oidc: user with email (=sub) "%s" not found', id_token.sub)
+            else:
+                user = users[0]
+                logger.info('auth_oidc: found user using email (=sub) "%s": %s', id_token.sub, user)
         else:
             try:
                 user = User.objects.get(
diff --git a/src/authentic2_auth_oidc/migrations/0004_auto_20171017_1522.py b/src/authentic2_auth_oidc/migrations/0004_auto_20171017_1522.py
index e195b54b..a62917c0 100644
--- a/src/authentic2_auth_oidc/migrations/0004_auto_20171017_1522.py
+++ b/src/authentic2_auth_oidc/migrations/0004_auto_20171017_1522.py
@@ -18,6 +18,7 @@ class Migration(migrations.Migration):
                     ('create', 'create'),
                     ('find-uuid', 'use sub to find existing user through UUID'),
                     ('find-username', 'use sub to find existing user through username'),
+                    ('find-email', 'use sub to find existing user through email'),
                     ('none', 'none'),
                 ],
             ),
diff --git a/src/authentic2_auth_oidc/models.py b/src/authentic2_auth_oidc/models.py
index 24b05346..c2cf06e6 100644
--- a/src/authentic2_auth_oidc/models.py
+++ b/src/authentic2_auth_oidc/models.py
@@ -42,12 +42,14 @@ class OIDCProvider(models.Model):
     STRATEGY_CREATE = 'create'
     STRATEGY_FIND_UUID = 'find-uuid'
     STRATEGY_FIND_USERNAME = 'find-username'
+    STRATEGY_FIND_EMAIL = 'find-email'
     STRATEGY_NONE = 'none'
 
     STRATEGIES = [
         (STRATEGY_CREATE, _('create')),
         (STRATEGY_FIND_UUID, _('use sub to find existing user through UUID')),
         (STRATEGY_FIND_USERNAME, _('use sub to find existing user through username')),
+        (STRATEGY_FIND_EMAIL, _('use sub to find existing user through email')),
         (STRATEGY_NONE, _('none')),
     ]
     ALGO_NONE = 0
diff --git a/tests/test_auth_oidc.py b/tests/test_auth_oidc.py
index 6930ddb9..26da5bd8 100644
--- a/tests/test_auth_oidc.py
+++ b/tests/test_auth_oidc.py
@@ -954,6 +954,45 @@ def test_strategy_find_username(app, caplog, code, oidc_provider, oidc_provider_
             response = app.get(login_callback_url(oidc_provider), params={'code': code, 'state': state})
 
 
+def test_strategy_find_email(app, caplog, code, oidc_provider, oidc_provider_jwkset, simple_user):
+    # no mapping please
+    OIDCClaimMapping.objects.all().delete()
+    oidc_provider.strategy = oidc_provider.STRATEGY_FIND_EMAIL
+    oidc_provider.save()
+
+    assert User.objects.count() == 1
+
+    response = app.get('/').maybe_follow()
+    assert oidc_provider.name in response.text
+    response = response.click(oidc_provider.name)
+    location = urllib.parse.urlparse(response.location)
+    query = QueryDict(location.query)
+    state = query['state']
+    nonce = query['nonce']
+
+    # sub=simple_user.uuid MUST not work
+    with utils.check_log(caplog, 'cannot create user'):
+        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, sub=simple_user.uuid, nonce=nonce):
+            response = app.get(login_callback_url(oidc_provider), params={'code': code, 'state': state})
+
+    # sub=john.doe, MUST not work
+    with utils.check_log(caplog, 'cannot create user'):
+        with oidc_provider_mock(
+            oidc_provider, oidc_provider_jwkset, code, nonce=nonce, sub=simple_user.username
+        ):
+            response = app.get(login_callback_url(oidc_provider), params={'code': code, 'state': state})
+
+    simple_user.email = 'john.doe@example.org'
+    simple_user.save()
+
+    # sub=john.doe@example.org, MUST work
+    with utils.check_log(caplog, 'found user using email'):
+        with oidc_provider_mock(
+            oidc_provider, oidc_provider_jwkset, code, nonce=nonce, sub=simple_user.email
+        ):
+            response = app.get(login_callback_url(oidc_provider), params={'code': code, 'state': state})
+
+
 def test_error_access_denied(app, caplog):
     oidc_provider = make_oidc_provider()
     response = app.get('/login/')
-- 
2.34.1

