Projet

Général

Profil

0001-idp_oidc-discard-any-extra-scopes-at-profile-selecti.patch

Paul Marillonnet, 13 avril 2022 15:16

Télécharger (7,93 ko)

Voir les différences:

Subject: [PATCH] idp_oidc: discard any extra scopes at profile selection
 (#62900)

 src/authentic2_idp_oidc/apps.py               | 25 +++++++
 .../authentic2_idp_oidc/authorization.html    | 13 +++-
 src/authentic2_idp_oidc/views.py              |  2 +
 tests/idp_oidc/test_user_profiles.py          | 66 ++++++++++++++++++-
 4 files changed, 103 insertions(+), 3 deletions(-)
src/authentic2_idp_oidc/apps.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import logging
18

  
17 19
import django.apps
18 20
from django.template.loader import render_to_string
19 21
from django.utils.encoding import smart_bytes
20 22

  
23
logger = logging.getLogger(__name__)
24

  
21 25

  
22 26
class Plugin:
23 27
    def logout_list(self, request):
......
157 161
        qs = qs.distinct()
158 162

  
159 163
        return qs
164

  
165
    def a2_hook_idp_oidc_modify_user_info(self, client, user, scope_set, user_info, profile=None):
166
        """Removes any extra scope and their associated claims whenever the user selects a profile"""
167
        from authentic2_idp_oidc.models import OIDCClaim
168

  
169
        extra_scopes = scope_set - {'openid', 'email', 'profile'}
170
        if profile and extra_scopes:
171
            logger.info(
172
                'idp_oidc: profile of type %s chosen by user %s for client %s, discarding scopes %s',
173
                profile.profile_type,
174
                user,
175
                client,
176
                ' '.join(extra_scopes),
177
            )
178
            scope_set -= extra_scopes
179
            extra_claims = []
180
            for claim in OIDCClaim.objects.filter(client=client, name__in=user_info.keys()):
181
                if set(claim.get_scopes()) & extra_scopes and not set(claim.get_scopes()) & scope_set:
182
                    extra_claims.append(claim.name)
183
            for claim in extra_claims:
184
                user_info.pop(claim)
src/authentic2_idp_oidc/templates/authentic2_idp_oidc/authorization.html
23 23
      </ul>
24 24
    {% endif %}
25 25
    {% if needs_profile_validation %}
26
     <div id="profile-validation">
27
     <p>{% trans "You may authenticate as owner of the following juridical entity management profile, which may change the aforementioned information." %}</p>
26
      <div id="profile-validation">
27
      <p>{% trans "You may authenticate as owner of the following management profile, which may change the aforementioned information." %}</p>
28
      {% if "email" in scopes %}
29
      <p>{% blocktrans with client_name=client.name %}In this case, {{ client_name }} will collect your managenement profile email address instead of your personal email address.{% endblocktrans %}</p>
30
      {% endif %}
31
      {% if "profile" in scopes %}
32
      <p>{% blocktrans with client_name=client.name %}{{ client_name }} will still collect your first name, your last name and your username.{% endblocktrans %}</p>
33
      {% endif %}
34
      {% if extra_scopes %}
35
      {% blocktrans with client_name=client.name %}Additional personal info will not be sent to {{ client_name }}.{% endblocktrans %}
36
      {% endif %}
28 37
      <div class="profile" id="profile-validation-none">
29 38
        <input type="radio" id="_none" name="profile-validation" value="" checked>
30 39
        <label for="_none">{% trans "Keep my default user profile." %}</label>
src/authentic2_idp_oidc/views.py
307 307
    if not scope:
308 308
        raise MissingParameter('scope')
309 309
    scopes = utils.scope_set(scope)
310
    extra_scopes = scopes - {'openid', 'email', 'profile'}
310 311
    if 'openid' not in scopes:
311 312
        raise InvalidScope(_('Scope must contain "openid", received "%s"') % ', '.join(sorted(scopes)))
312 313
    if not is_scopes_allowed(scopes, client):
......
438 439
                        'needs_scope_validation': needs_scope_validation,
439 440
                        'client': client,
440 441
                        'scopes': scopes - {'openid'},
442
                        'extra_scopes': extra_scopes,
441 443
                    },
442 444
                )
443 445
    if response_type == 'code':
tests/idp_oidc/test_user_profiles.py
30 30

  
31 31
from authentic2.custom_user.models import Profile, ProfileType
32 32
from authentic2.utils.misc import make_url
33
from authentic2_idp_oidc.models import OIDCAccessToken, OIDCAuthorization, OIDCCode
33
from authentic2_idp_oidc.models import OIDCAccessToken, OIDCAuthorization, OIDCClaim, OIDCCode
34 34
from authentic2_idp_oidc.utils import get_jwkset, make_pairwise_sub, make_sub, reverse_pairwise_sub
35 35

  
36 36
from .. import utils
......
471 471
    )
472 472
    response = app.get(authorize_url)
473 473
    assert 'do_not_ask_again' not in response.text
474

  
475

  
476
def test_scopes_minimization(app, oidc_client, profile_user, profile_settings):
477
    oidc_client.idtoken_algo = oidc_client.ALGO_HMAC
478
    oidc_client.activate_user_profiles = True
479
    oidc_client.perform_sub_profile_substitution = True
480
    oidc_client.scope = 'openid profile email custom1 custom2 custom3'
481
    oidc_client.save()
482
    redirect_uri = oidc_client.redirect_uris.split()[0]
483

  
484
    oidcclaim_mapping = {
485
        'custom1': 'first_name',
486
        'custom2': 'last_name',
487
        'custom3': 'is_active',
488
    }
489
    for claim, value in oidcclaim_mapping.items():
490
        OIDCClaim.objects.create(
491
            client=oidc_client,
492
            name=claim,
493
            scopes=claim,
494
            value=value,
495
        )
496

  
497
    params = {
498
        'client_id': oidc_client.client_id,
499
        'scope': 'openid profile email custom1 custom2 custom3',
500
        'redirect_uri': redirect_uri,
501
        'state': 'xxx',
502
        'nonce': 'yyy',
503
        'login_hint': 'backoffice john@example.com',
504
        'response_type': 'code',
505
    }
506

  
507
    assert not OIDCCode.objects.count()
508
    assert not OIDCAuthorization.objects.count()
509

  
510
    authorize_url = make_url('oidc-authorize', params=params)
511
    utils.login(app, profile_user)
512
    response = app.get(authorize_url)
513
    response.form.set('profile-validation', profile_user.profiles.first().id)
514
    response = response.form.submit('accept')
515
    location = urllib.parse.urlparse(response['Location'])
516
    query = urllib.parse.parse_qs(location.query)
517
    code = query['code'][0]
518

  
519
    token_url = make_url('oidc-token')
520
    response = app.post(
521
        token_url,
522
        params={
523
            'grant_type': 'authorization_code',
524
            'code': code,
525
            'redirect_uri': oidc_client.redirect_uris.split()[0],
526
        },
527
        headers=client_authentication_headers(oidc_client),
528
    )
529
    assert response.json['access_token']
530
    id_token = response.json['id_token']
531
    k = base64.b64encode(oidc_client.client_secret.encode('utf-8'))
532
    key = JWK(kty='oct', k=force_text(k))
533
    algs = ['HS256']
534
    jwt = JWT(jwt=id_token, key=key, algs=algs)
535
    claims = json.loads(jwt.claims)
536

  
537
    assert not set(oidcclaim_mapping.keys()) & set(claims.keys())
474
-