0001-idp_oidc-make-sub-depend-on-user-s-profile-choice-du.patch
src/authentic2/app_settings.py | ||
---|---|---|
333 | 333 |
default=250, |
334 | 334 |
definition='Maximum number of mails to send per period', |
335 | 335 |
), |
336 |
A2_USER_PROFILE_MANAGEMENT=Setting( |
|
337 |
default=False, |
|
338 |
definition='Activate user profiles for juridical entity management.', |
|
339 |
), |
|
336 | 340 |
) |
337 | 341 | |
338 | 342 |
app_settings = AppSettings(default_settings) |
src/authentic2_idp_oidc/apps.py | ||
---|---|---|
88 | 88 |
sub = smart_bytes(view.kwargs[lookup_url_kwarg]) |
89 | 89 |
decrypted = utils.reverse_pairwise_sub(client, sub) |
90 | 90 |
if decrypted: |
91 |
view.kwargs[lookup_url_kwarg] = uuid.UUID(bytes=decrypted).hex |
|
91 |
view.kwargs[lookup_url_kwarg] = uuid.UUID(bytes=decrypted.split(b'#')[0]).hex
|
|
92 | 92 | |
93 | 93 |
def a2_hook_api_modify_serializer_after_validation(self, view, serializer): |
94 | 94 |
import uuid |
... | ... | |
111 | 111 |
for u in serializer.validated_data['known_uuids']: |
112 | 112 |
decrypted = utils.reverse_pairwise_sub(client, smart_bytes(u)) |
113 | 113 |
if decrypted: |
114 |
new_known_uuid = uuid.UUID(bytes=decrypted).hex |
|
114 |
new_known_uuid = uuid.UUID(bytes=decrypted.split(b'#')[0]).hex
|
|
115 | 115 |
new_known_uuids.append(new_known_uuid) |
116 | 116 |
uuid_map[new_known_uuid] = u |
117 | 117 |
else: |
src/authentic2_idp_oidc/migrations/0015_oidccode_profile_id.py | ||
---|---|---|
1 |
# Generated by Django 2.2.24 on 2022-02-14 09:57 |
|
2 | ||
3 |
from django.db import migrations, models |
|
4 | ||
5 | ||
6 |
class Migration(migrations.Migration): |
|
7 | ||
8 |
dependencies = [ |
|
9 |
('authentic2_idp_oidc', '0014_auto_20201126_1812'), |
|
10 |
] |
|
11 | ||
12 |
operations = [ |
|
13 |
migrations.AddField( |
|
14 |
model_name='oidccode', |
|
15 |
name='profile_id', |
|
16 |
field=models.PositiveIntegerField(null=True, verbose_name='profile identifier'), |
|
17 |
), |
|
18 |
] |
src/authentic2_idp_oidc/models.py | ||
---|---|---|
288 | 288 |
uuid = models.CharField(max_length=128, verbose_name=_('uuid'), default=generate_uuid) |
289 | 289 |
client = models.ForeignKey(to=OIDCClient, verbose_name=_('client'), on_delete=models.CASCADE) |
290 | 290 |
user = models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name=_('user'), on_delete=models.CASCADE) |
291 |
profile_id = models.PositiveIntegerField(verbose_name=_('profile identifier'), null=True) |
|
291 | 292 |
scopes = models.TextField(verbose_name=_('scopes')) |
292 | 293 |
state = models.TextField(null=True, verbose_name=_('state')) |
293 | 294 |
nonce = models.TextField(null=True, verbose_name=_('nonce')) |
src/authentic2_idp_oidc/templates/authentic2_idp_oidc/authorization.html | ||
---|---|---|
16 | 16 |
{% endfor %} |
17 | 17 |
</ul> |
18 | 18 |
{% endif %} |
19 |
{% if needs_profile_validation %} |
|
20 |
<p>{% trans "Additionally, you may authenticate as owner of the following juridical entity management profile, which may change the aforementioned information:" %}</p> |
|
21 |
<div id="profile-validation-none"> |
|
22 |
<input type="radio" id="_none" name="profile-validation" value=""> |
|
23 |
<label for="_none">{% trans "Keep my default user profile." %}</label> |
|
24 |
</div> |
|
25 |
{% for profile in user.profiles %} |
|
26 |
<div id="profile-validation-{{ profile.id }}"> |
|
27 |
<input type="radio" id="{{ profile.id }}" name="profile-validation" value="{{ profile.id }}"> |
|
28 |
<label for="{{ profile.slug }}">{% blocktrans with type_name=profile.profile_type.name %}Profile of type {{ type_name }}.{% endblocktrans %}</label> |
|
29 |
</div> |
|
30 |
{% endfor %} |
|
31 |
{% endif %} |
|
19 | 32 |
{% csrf_token %} |
20 | 33 |
<p class="a2-oidc-authorization-form--do-not-ask-again"> |
21 | 34 |
<label for="id_do_not_ask_again"><input id="id_do_not_ask_again" type="checkbox" name="do_not_ask_again" value="on"/><span>{% trans "Do not ask again" %}</span></label> |
src/authentic2_idp_oidc/utils.py | ||
---|---|---|
121 | 121 |
return urllib.parse.urlparse(url).netloc.split(':')[0] |
122 | 122 | |
123 | 123 | |
124 |
def make_sub(client, user): |
|
124 |
def make_sub(client, user, profile_id=None):
|
|
125 | 125 |
if client.identifier_policy in (client.POLICY_PAIRWISE, client.POLICY_PAIRWISE_REVERSIBLE): |
126 |
return make_pairwise_sub(client, user) |
|
126 |
return make_pairwise_sub(client, user, profile_id=profile_id)
|
|
127 | 127 |
elif client.identifier_policy == client.POLICY_UUID: |
128 | 128 |
return force_text(user.uuid) |
129 | 129 |
elif client.identifier_policy == client.POLICY_EMAIL: |
... | ... | |
132 | 132 |
raise NotImplementedError |
133 | 133 | |
134 | 134 | |
135 |
def make_pairwise_sub(client, user): |
|
135 |
def make_pairwise_sub(client, user, profile_id=None):
|
|
136 | 136 |
'''Make a pairwise sub''' |
137 | 137 |
if client.identifier_policy == client.POLICY_PAIRWISE: |
138 |
return make_pairwise_unreversible_sub(client, user) |
|
138 |
return make_pairwise_unreversible_sub(client, user, profile_id=profile_id)
|
|
139 | 139 |
elif client.identifier_policy == client.POLICY_PAIRWISE_REVERSIBLE: |
140 |
return make_pairwise_reversible_sub(client, user) |
|
140 |
return make_pairwise_reversible_sub(client, user, profile_id=profile_id)
|
|
141 | 141 |
else: |
142 | 142 |
raise NotImplementedError('unknown pairwise client.identifier_policy %s' % client.identifier_policy) |
143 | 143 | |
144 | 144 | |
145 |
def make_pairwise_unreversible_sub(client, user): |
|
145 |
def make_pairwise_unreversible_sub(client, user, profile_id=None):
|
|
146 | 146 |
sector_identifier = client.get_sector_identifier() |
147 | 147 |
sub = sector_identifier + str(user.uuid) + settings.SECRET_KEY |
148 |
if profile_id is not None: |
|
149 |
sub += str(profile_id) |
|
148 | 150 |
sub = base64.b64encode(hashlib.sha256(sub.encode('utf-8')).digest()) |
149 | 151 |
return sub.decode('utf-8') |
150 | 152 | |
151 | 153 | |
152 |
def make_pairwise_reversible_sub(client, user): |
|
153 |
return make_pairwise_reversible_sub_from_uuid(client, user.uuid) |
|
154 |
def make_pairwise_reversible_sub(client, user, profile_id=None):
|
|
155 |
return make_pairwise_reversible_sub_from_uuid(client, user.uuid, profile_id=profile_id)
|
|
154 | 156 | |
155 | 157 | |
156 |
def make_pairwise_reversible_sub_from_uuid(client, user_uuid): |
|
158 |
def make_pairwise_reversible_sub_from_uuid(client, user_uuid, profile_id=None):
|
|
157 | 159 |
try: |
158 | 160 |
identifier = uuid.UUID(user_uuid).bytes |
159 | 161 |
except ValueError: |
160 | 162 |
return None |
161 | 163 |
sector_identifier = client.get_sector_identifier() |
162 |
return crypto.aes_base64url_deterministic_encrypt( |
|
163 |
settings.SECRET_KEY.encode('utf-8'), identifier, sector_identifier |
|
164 |
).decode('utf-8') |
|
164 |
cipher_args = [ |
|
165 |
settings.SECRET_KEY.encode('utf-8'), |
|
166 |
identifier, |
|
167 |
sector_identifier, |
|
168 |
] |
|
169 |
if profile_id is not None: |
|
170 |
cipher_args[1] += '#%s' % str(profile_id) |
|
171 |
return crypto.aes_base64url_deterministic_encrypt(*cipher_args).decode('utf-8') |
|
165 | 172 | |
166 | 173 | |
167 | 174 |
def reverse_pairwise_sub(client, sub): |
... | ... | |
184 | 191 |
return str(value) |
185 | 192 | |
186 | 193 | |
187 |
def create_user_info(request, client, user, scope_set, id_token=False): |
|
194 |
def create_user_info(request, client, user, scope_set, id_token=False, profile_id=None):
|
|
188 | 195 |
'''Create user info dictionary''' |
189 | 196 |
user_info = {} |
190 | 197 |
if 'openid' in scope_set: |
191 |
user_info['sub'] = make_sub(client, user) |
|
198 |
user_info['sub'] = make_sub(client, user, profile_id=profile_id)
|
|
192 | 199 |
attributes = get_attributes( |
193 | 200 |
{ |
194 | 201 |
'user': user, |
src/authentic2_idp_oidc/views.py | ||
---|---|---|
38 | 38 |
from authentic2 import app_settings as a2_app_settings |
39 | 39 |
from authentic2 import hooks |
40 | 40 |
from authentic2.a2_rbac.models import OrganizationalUnit |
41 |
from authentic2.custom_user.models import Profile |
|
41 | 42 |
from authentic2.decorators import setting_enabled |
42 | 43 |
from authentic2.exponential_retry_timeout import ExponentialRetryTimeout |
43 | 44 |
from authentic2.utils.misc import last_authentication_event, login_require, make_url, redirect |
... | ... | |
359 | 360 | |
360 | 361 |
iat = now() # iat = issued at |
361 | 362 | |
363 |
needs_profile_validation = False |
|
364 |
profile = None |
|
365 |
if request.user.subprofiles and a2_app_settings.A2_USER_PROFILE_MANAGEMENT: |
|
366 |
needs_profile_validation = True |
|
362 | 367 |
if client.authorization_mode != client.AUTHORIZATION_MODE_NONE or 'consent' in prompt: |
363 | 368 |
# authorization by user is mandatory, as per local configuration or per explicit request by |
364 | 369 |
# the RP |
... | ... | |
381 | 386 |
authorized_scopes = set() |
382 | 387 |
for authorization in qs: |
383 | 388 |
authorized_scopes |= authorization.scope_set() |
384 |
if (authorized_scopes & scopes) < scopes: |
|
389 |
if (authorized_scopes & scopes) < scopes or needs_profile_validation:
|
|
385 | 390 |
if 'none' in prompt: |
386 | 391 |
raise ConsentRequired(_('Consent is required but prompt parameter is "none"')) |
387 | 392 |
if request.method == 'POST': |
393 |
if request.POST.get('profile-validation', ''): |
|
394 |
try: |
|
395 |
profile = Profile.objects.get( |
|
396 |
user=request.user, |
|
397 |
slug=request.POST['profile-validation'], |
|
398 |
) |
|
399 |
except Profile.DoesNotExist: |
|
400 |
pass |
|
388 | 401 |
if 'accept' in request.POST: |
389 | 402 |
if 'do_not_ask_again' in request.POST: |
390 | 403 |
pk_to_deletes = [] |
... | ... | |
417 | 430 |
request, |
418 | 431 |
'authentic2_idp_oidc/authorization.html', |
419 | 432 |
{ |
433 |
'needs_profile_validation': needs_profile_validation, |
|
420 | 434 |
'client': client, |
421 | 435 |
'scopes': scopes - {'openid'}, |
422 | 436 |
}, |
... | ... | |
425 | 439 |
code = models.OIDCCode.objects.create( |
426 | 440 |
client=client, |
427 | 441 |
user=request.user, |
442 |
profile_id=profile.id if profile else None, |
|
428 | 443 |
scopes=' '.join(scopes), |
429 | 444 |
state=state, |
430 | 445 |
nonce=nonce, |
... | ... | |
721 | 736 |
): |
722 | 737 |
acr = '1' |
723 | 738 |
# prefill id_token with user info |
724 |
id_token = utils.create_user_info(request, client, oidc_code.user, oidc_code.scope_set(), id_token=True) |
|
739 |
id_token = utils.create_user_info( |
|
740 |
request, |
|
741 |
client, |
|
742 |
oidc_code.user, |
|
743 |
oidc_code.scope_set(), |
|
744 |
id_token=True, |
|
745 |
profile_id=oidc_code.profile_id, |
|
746 |
) |
|
725 | 747 |
exp = start + idtoken_duration(client) |
726 | 748 |
id_token.update( |
727 | 749 |
{ |
728 |
- |