-
Notifications
You must be signed in to change notification settings - Fork 336
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature] Normalize OSF Verification Key [#OSF-6560] #5964
Changes from 36 commits
c1bf666
16bab4f
01c28f7
9a6b2ba
7437496
ae0c145
7ecf373
b1a6581
c9dc259
c8a0fac
f46c8a0
d459d8f
2e4ea05
617d2eb
6be7705
e538758
93d947e
d6e09ab
a3114d4
9ceb096
7968675
810abe3
12d1308
9f3b5c5
28f8f83
52836d8
96b0c5e
1311351
f627c6b
8ad4a94
042e2ca
6dd18d2
0ca06e0
d9a5d64
7e862d9
576afb0
ffc82ec
6a95638
3385da7
bb65649
31d08eb
f357253
474c1a2
d589f24
c8bb69b
e3a4137
10419b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,95 +36,74 @@ | |
|
||
|
||
@collect_auth | ||
def reset_password_get(auth, verification_key=None, **kwargs): | ||
def reset_password_get(auth, uid=None, token=None): | ||
""" | ||
View for user to land on the reset password page. | ||
HTTp Method: GET | ||
|
||
:raises: HTTPError(http.BAD_REQUEST) if verification_key is invalid | ||
:param auth: the authentication state | ||
:param uid: the user id | ||
:param token: the token in verification key | ||
:return | ||
:raises: HTTPError(http.BAD_REQUEST) if verification key for the user is invalid or has expired | ||
""" | ||
|
||
# If user is already logged in, log user out | ||
# TODO: discuss this with @MattF | ||
# if users are logged in, log them out and redirect back to this page | ||
if auth.logged_in: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "log user out and redirect back" is more secure and less confusing than just "go to dashboard" |
||
return auth_logout(redirect_url=request.url) | ||
|
||
# Check if request bears a valid verification_key | ||
user_obj = get_user(verification_key=verification_key) | ||
if not user_obj: | ||
# Check if request bears a valid pair of `uid` and `token` | ||
user_obj = User.load(uid) | ||
if not (user_obj and user_obj.verify_password_token(token=token)): | ||
# TODO: do we want to reveal detailed error message to the client? | ||
error_data = { | ||
'message_short': 'Invalid url.', | ||
'message_long': 'The verification key in the URL is invalid or has expired.' | ||
'message_short': 'Invalid Request.', | ||
'message_long': 'The request URL is invalid, has been expired or already used', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did product request this language change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, but I will talk with them on this change. The old message is no longer proper after our verification key normalization. |
||
} | ||
raise HTTPError(400, data=error_data) | ||
|
||
return { | ||
'verification_key': verification_key, | ||
} | ||
raise HTTPError(http.BAD_REQUEST, data=error_data) | ||
|
||
|
||
@collect_auth | ||
def reset_password(auth, **kwargs): | ||
""" Show reset password page. | ||
""" | ||
if auth.logged_in: | ||
return auth_logout(redirect_url=request.url) | ||
verification_key = kwargs['verification_key'] | ||
|
||
# Check if request bears a valid verification_key | ||
user_obj = get_user(verification_key=verification_key) | ||
if not user_obj: | ||
error_data = { | ||
'message_short': 'Invalid url.', | ||
'message_long': 'The verification key in the URL is invalid or has expired.' | ||
} | ||
raise HTTPError(400, data=error_data) | ||
# refresh the verification key (v2) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [To CR] Deprecated Code |
||
user_obj.verification_key_v2 = generate_verification_key(verification_type='password') | ||
user_obj.save() | ||
|
||
return { | ||
'verification_key': verification_key | ||
'uid': user_obj._id, | ||
'token': user_obj.verification_key_v2['token'], | ||
} | ||
|
||
|
||
@collect_auth | ||
def forgot_password_get(auth, **kwargs): | ||
""" | ||
View to user to land on forgot password page. | ||
HTTP Method: GET | ||
""" | ||
|
||
# If user is already logged in, redirect to dashboard page. | ||
if auth.logged_in: | ||
return redirect(web_url_for('dashboard')) | ||
|
||
return {} | ||
|
||
|
||
@collect_auth | ||
def reset_password_post(auth, verification_key=None, **kwargs): | ||
def reset_password_post(uid=None, token=None): | ||
""" | ||
View for user to submit reset password form. | ||
HTTP Method: POST | ||
|
||
:param uid: the user id | ||
:param token: the token in verification key | ||
:return: | ||
:raises: HTTPError(http.BAD_REQUEST) if verification_key is invalid | ||
""" | ||
|
||
# If user is already logged in, log user out | ||
if auth.logged_in: | ||
return auth_logout(redirect_url=request.url) | ||
|
||
form = ResetPasswordForm(request.form) | ||
|
||
# Check if request bears a valid verification_key | ||
user_obj = get_user(verification_key=verification_key) | ||
if not user_obj: | ||
# Check if request bears a valid pair of `uid` and `token` | ||
user_obj = User.load(uid) | ||
if not (user_obj and user_obj.verify_password_token(token=token)): | ||
# TODO: do we want to reveal detailed error message to the client? | ||
error_data = { | ||
'message_short': 'Invalid url.', | ||
'message_long': 'The verification key in the URL is invalid or has expired.' | ||
'message_short': 'Invalid Request.', | ||
'message_long': 'The request URL is invalid, has been expired or already used', | ||
} | ||
raise HTTPError(400, data=error_data) | ||
raise HTTPError(http.BAD_REQUEST, data=error_data) | ||
|
||
if form.validate(): | ||
# new random verification key, allows CAS to authenticate the user w/o password, one-time only. | ||
# this overwrite also invalidates the verification key generated by forgot_password_post | ||
user_obj.verification_key = generate_verification_key() | ||
if not form.validate(): | ||
# Don't go anywhere | ||
forms.push_errors_to_status(form.errors) | ||
else: | ||
# clear verification key (v2) | ||
user_obj.verification_key_v2 = {} | ||
# new verification key (v1) for CAS | ||
user_obj.verification_key = generate_verification_key(verification_type=None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cas login with username and verification key still uses v1 given it is instant verification |
||
try: | ||
user_obj.set_password(form.password.data) | ||
user_obj.save() | ||
|
@@ -133,71 +112,92 @@ def reset_password_post(auth, verification_key=None, **kwargs): | |
status.push_status_message(message, kind='warning', trust=False) | ||
else: | ||
status.push_status_message('Password reset', kind='success', trust=False) | ||
# redirect to CAS and authenticate the user with the one-time verification key. | ||
# redirect to CAS and authenticate the user automatically with one-time verification key. | ||
return redirect(cas.get_login_url( | ||
web_url_for('user_account', _absolute=True), | ||
username=user_obj.username, | ||
verification_key=user_obj.verification_key | ||
)) | ||
else: | ||
forms.push_errors_to_status(form.errors) | ||
# Don't go anywhere | ||
|
||
return { | ||
'verification_key': verification_key | ||
}, 400 | ||
'uid': user_obj._id, | ||
'token': user_obj.verification_key_v2['token'], | ||
} | ||
|
||
|
||
@collect_auth | ||
def forgot_password_post(auth, **kwargs): | ||
def forgot_password_get(auth): | ||
""" | ||
View for user to submit forgot password form. | ||
HTTP Method: POST | ||
View for user to land on the forgot password page. | ||
HTTP Method: GET | ||
|
||
:param auth: the authentication context | ||
:return | ||
""" | ||
|
||
# If user is already logged in, redirect to dashboard page. | ||
# if users are logged in, log them out and redirect back to this page | ||
if auth.logged_in: | ||
return redirect(web_url_for('dashboard')) | ||
return auth_logout(redirect_url=request.url) | ||
|
||
return {} | ||
|
||
|
||
def forgot_password_post(): | ||
""" | ||
View for user to submit forgot password form. | ||
HTTP Method: POST | ||
:return {} | ||
""" | ||
|
||
form = ForgotPasswordForm(request.form, prefix='forgot_password') | ||
|
||
if form.validate(): | ||
if not form.validate(): | ||
# Don't go anywhere | ||
forms.push_errors_to_status(form.errors) | ||
else: | ||
email = form.email.data | ||
status_message = ('If there is an OSF account associated with {0}, an email with instructions on how to ' | ||
'reset the OSF password has been sent to {0}. If you do not receive an email and believe ' | ||
'you should have, please contact OSF Support. ').format(email) | ||
# check if the user exists | ||
user_obj = get_user(email=email) | ||
if user_obj: | ||
# check forgot_password rate limit | ||
if throttle_period_expired(user_obj.email_last_sent, settings.SEND_EMAIL_THROTTLE): | ||
# new random verification key, allows OSF to check whether the reset_password request is valid, | ||
# this verification key is used twice, one for GET reset_password and one for POST reset_password | ||
# and it will be destroyed when POST reset_password succeeds | ||
user_obj.verification_key = generate_verification_key() | ||
user_obj.email_last_sent = datetime.datetime.utcnow() | ||
user_obj.save() | ||
reset_link = furl.urljoin( | ||
settings.DOMAIN, | ||
web_url_for( | ||
'reset_password_get', | ||
verification_key=user_obj.verification_key | ||
) | ||
) | ||
mails.send_mail( | ||
to_addr=email, | ||
mail=mails.FORGOT_PASSWORD, | ||
reset_link=reset_link | ||
) | ||
status.push_status_message(status_message, kind='success', trust=False) | ||
else: | ||
if not user_obj: | ||
# do not reveal user existence information by pushing success message even when user not found | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be accomplished without code duplication by putting the call to |
||
status.push_status_message(status_message, kind='success', trust=False) | ||
else: | ||
# rate limit forgot_password_post | ||
if not throttle_period_expired(user_obj.email_last_sent, settings.SEND_EMAIL_THROTTLE): | ||
status.push_status_message('You have recently requested to change your password. Please wait a ' | ||
'few minutes before trying again.', kind='error', trust=False) | ||
else: | ||
status.push_status_message(status_message, kind='success', trust=False) | ||
else: | ||
forms.push_errors_to_status(form.errors) | ||
# Don't go anywhere | ||
else: | ||
# TODO [OSF-6673]: Use the feature in [OSF-6998] for user to resend claim email. | ||
# if the user account is not claimed yet | ||
if (user_obj.is_invited and | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mfraezz this part is added by @chennan47 as a quick fix for issue https://openscience.atlassian.net/browse/OSF-6673. |
||
user_obj.unclaimed_records and | ||
not user_obj.date_last_login and | ||
not user_obj.is_claimed and | ||
not user_obj.is_registered): | ||
status.push_status_message('You cannot reset password on this account. Please contact OSF Support.', | ||
kind='error', trust=False) | ||
else: | ||
# new random verification key (v2) | ||
user_obj.verification_key_v2 = generate_verification_key(verification_type='password') | ||
user_obj.email_last_sent = datetime.datetime.utcnow() | ||
user_obj.save() | ||
reset_link = furl.urljoin( | ||
settings.DOMAIN, | ||
web_url_for( | ||
'reset_password_get', | ||
uid=user_obj._id, | ||
token=user_obj.verification_key_v2['token'] | ||
) | ||
) | ||
mails.send_mail( | ||
to_addr=email, | ||
mail=mails.FORGOT_PASSWORD, | ||
reset_link=reset_link | ||
) | ||
status.push_status_message(status_message, kind='success', trust=False) | ||
|
||
return {} | ||
|
||
|
@@ -546,17 +546,25 @@ def unconfirmed_email_add(auth=None): | |
}, 200 | ||
|
||
|
||
def send_confirm_email(user, email, external_id_provider=None, external_id=None): | ||
def send_confirm_email(user, email, renew=False, external_id_provider=None, external_id=None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when user asks to resend confirmation email, refresh the token with |
||
""" | ||
Sends a confirmation email to `user` to a given email. | ||
Sends `user` a confirmation to the given `email`. | ||
|
||
|
||
:param user: the user | ||
:param email: the email | ||
:param renew: refresh the token | ||
:param external_id_provider: user's external id provider | ||
:param external_id: user's external id | ||
:return: | ||
:raises: KeyError if user does not have a confirmation token for the given email. | ||
""" | ||
|
||
confirmation_url = user.get_confirmation_url( | ||
email, | ||
external=True, | ||
force=True, | ||
renew=renew, | ||
external_id_provider=external_id_provider | ||
) | ||
|
||
|
@@ -705,7 +713,7 @@ def resend_confirmation_post(auth): | |
if user: | ||
if throttle_period_expired(user.email_last_sent, settings.SEND_EMAIL_THROTTLE): | ||
try: | ||
send_confirm_email(user, clean_email) | ||
send_confirm_email(user, clean_email, renew=True) | ||
except KeyError: | ||
# already confirmed, redirect to dashboard | ||
status_message = 'This email {0} has already been confirmed.'.format(clean_email) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
from datetime import datetime | ||
|
||
import httplib as http | ||
import urllib | ||
import urlparse | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please ignore this
# TODO
, it was supposed to be added here https://github.com/CenterForOpenScience/osf.io/pull/5964/files#diff-9a64b4982d0bd80f7a498c65ddce2023R138.As discussed on Friday, the decision is to log users out if they are already logged in instead of redirecting them to dashboard for both
reset_password_get
andforgot_password_get
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be removed, then?