0001-idp_oidc-discard-any-extra-scopes-at-profile-selecti.patch
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 |
- |