|
68 | 68 | UserChangePasswordSerializer,
|
69 | 69 | UserMessageSerializer,
|
70 | 70 | ExternalLoginSerialiser,
|
71 |
| - ExternalLoginConfirmEmailSerializer, |
| 71 | + ConfirmEmailSerializer, |
72 | 72 | )
|
73 | 73 | from django.contrib.auth.models import AnonymousUser
|
74 | 74 | from django.http import JsonResponse
|
|
80 | 80 | from framework.auth.oauth_scopes import CoreScopes, normalize_scopes
|
81 | 81 | from framework.auth.exceptions import ChangePasswordError
|
82 | 82 | from framework.celery_tasks.handlers import enqueue_task
|
| 83 | +from framework.flask import redirect |
83 | 84 | from framework.utils import throttle_period_expired
|
84 | 85 | from framework.sessions.utils import remove_sessions_for_user
|
85 | 86 | from framework.exceptions import PermissionsError, HTTPError
|
|
105 | 106 | from website import mails, settings, language
|
106 | 107 | from website.project.views.contributor import send_claim_email, send_claim_registered_email
|
107 | 108 | from website.util.metrics import CampaignClaimedTags, CampaignSourceTags
|
108 |
| -from framework.auth import exceptions |
| 109 | +from framework.auth import exceptions, cas |
109 | 110 |
|
110 | 111 |
|
111 | 112 | class UserMixin:
|
@@ -1060,6 +1061,90 @@ def post(self, request, *args, **kwargs):
|
1060 | 1061 | return Response(status=status.HTTP_204_NO_CONTENT)
|
1061 | 1062 |
|
1062 | 1063 |
|
| 1064 | +class ConfirmEmailView(JSONAPIBaseView, generics.CreateAPIView): |
| 1065 | + """ |
| 1066 | + Confirm an e-mail address created during *first-time* OAuth login. |
| 1067 | +
|
| 1068 | + **URL:** POST /v2/users/external_login/confirm_email/ |
| 1069 | +
|
| 1070 | + **Body (JSON):** |
| 1071 | + { |
| 1072 | + "uid": "<osf_user_id>", |
| 1073 | + "token": "<email_verification_token>", |
| 1074 | + "destination": "<campaign-code or relative URL>" |
| 1075 | + } |
| 1076 | +
|
| 1077 | + On success the endpoint redirects (HTTP 302) to CAS with a |
| 1078 | + one-time verification key, exactly like the original Flask view. |
| 1079 | + """ |
| 1080 | + permission_classes = ( |
| 1081 | + drf_permissions.IsAuthenticatedOrReadOnly, |
| 1082 | + base_permissions.TokenHasScope, |
| 1083 | + ) |
| 1084 | + serializer_class = ConfirmEmailSerializer |
| 1085 | + throttle_classes = (NonCookieAuthThrottle, BurstRateThrottle, RootAnonThrottle) |
| 1086 | + view_category = 'users' |
| 1087 | + view_name = 'confirm-user' |
| 1088 | + |
| 1089 | + def post(self, request, *args, **kwargs): |
| 1090 | + data = self.get_serializer(data=request.data) |
| 1091 | + data.is_valid(raise_exception=True) |
| 1092 | + |
| 1093 | + uid = data.validated_data['uid'] |
| 1094 | + token = data.validated_data['token'] |
| 1095 | + |
| 1096 | + user = OSFUser.load(uid) |
| 1097 | + if not user: |
| 1098 | + raise ValidationError('User not found.') |
| 1099 | + |
| 1100 | + verification = user.email_verifications.get(token) |
| 1101 | + if not verification: |
| 1102 | + raise ValidationError('Invalid or expired token.') |
| 1103 | + |
| 1104 | + provider = next(iter(verification['external_identity'])) |
| 1105 | + provider_id = next(iter(verification['external_identity'][provider])) |
| 1106 | + |
| 1107 | + if provider not in user.external_identity: |
| 1108 | + raise ValidationError('External-ID provider mismatch.') |
| 1109 | + |
| 1110 | + external_status = user.external_identity[provider][provider_id] |
| 1111 | + ensure_external_identity_uniqueness(provider, provider_id, user) |
| 1112 | + |
| 1113 | + email = verification['email'] |
| 1114 | + if not user.is_registered: |
| 1115 | + user.register(email) |
| 1116 | + |
| 1117 | + user.emails.get_or_create(address=email.lower()) |
| 1118 | + user.external_identity[provider][provider_id] = 'VERIFIED' |
| 1119 | + user.date_last_logged_in = timezone.now() |
| 1120 | + |
| 1121 | + del user.email_verifications[token] |
| 1122 | + user.verification_key = generate_verification_key() |
| 1123 | + user.save() |
| 1124 | + |
| 1125 | + service_url = request.build_absolute_uri() |
| 1126 | + if external_status == 'CREATE': |
| 1127 | + service_url += '&' + urlencode({'new': 'true'}) |
| 1128 | + elif external_status == 'LINK': |
| 1129 | + mails.send_mail( |
| 1130 | + user=user, |
| 1131 | + to_addr=user.username, |
| 1132 | + mail=mails.EXTERNAL_LOGIN_LINK_SUCCESS, |
| 1133 | + external_id_provider=provider, |
| 1134 | + can_change_preferences=False, |
| 1135 | + ) |
| 1136 | + |
| 1137 | + enqueue_task(update_affiliation_for_orcid_sso_users.s(user._id, provider_id)) |
| 1138 | + |
| 1139 | + return redirect( |
| 1140 | + cas.get_login_url( |
| 1141 | + service_url, |
| 1142 | + username=user.username, |
| 1143 | + verification_key=user.verification_key, |
| 1144 | + ), |
| 1145 | + ) |
| 1146 | + |
| 1147 | + |
1063 | 1148 | class UserEmailsList(JSONAPIBaseView, generics.ListAPIView, generics.CreateAPIView, UserMixin, ListFilterMixin):
|
1064 | 1149 | permission_classes = (
|
1065 | 1150 | drf_permissions.IsAuthenticatedOrReadOnly,
|
@@ -1212,7 +1297,7 @@ class ExternalLoginConfirmEmailView(generics.CreateAPIView):
|
1212 | 1297 | permission_classes = (
|
1213 | 1298 | drf_permissions.AllowAny,
|
1214 | 1299 | )
|
1215 |
| - serializer_class = ExternalLoginConfirmEmailSerializer |
| 1300 | + serializer_class = ConfirmEmailSerializer |
1216 | 1301 | view_category = 'users'
|
1217 | 1302 | view_name = 'external-login-confirm-email'
|
1218 | 1303 | throttle_classes = (NonCookieAuthThrottle, BurstRateThrottle, RootAnonThrottle)
|
|
0 commit comments