def test_verify_token_no_data(client): assert ( verify_token_no_data(encrypted_jwt_token(username="******", sensitive_content=None)) is None ) assert verify_token_no_data(jwt_token(username="******")) is None assert verify_token_no_data(encrypted_jwt_token(username="", sensitive_content=None)) is None assert verify_token_no_data(jwt_token(username="")) is None user = verify_token_no_data(encrypted_jwt_token(username="******", sensitive_content=None)) assert user.username == "unitadmin" user = verify_token_no_data(jwt_token(username="******")) assert user.username == "unitadmin"
def fill_basic_db(db): """ Fill the database with basic data. """ units, users, projects = add_data_to_db() db.session.add_all(units) db.session.add_all(users) db.session.commit() generate_project_key_pair(users[2], units[0].projects[0]) generate_project_key_pair(users[2], units[0].projects[2]) generate_project_key_pair(users[2], units[0].projects[4]) generate_project_key_pair(users[3], units[0].projects[1]) generate_project_key_pair(users[3], units[0].projects[3]) db.session.commit() user2_token = encrypted_jwt_token( username=users[2].username, sensitive_content="password", ) share_project_private_key( from_user=users[2], to_another=users[0], from_user_token=user2_token, project=projects[0], ) share_project_private_key( from_user=users[2], to_another=users[1], from_user_token=user2_token, project=projects[0], ) user3_token = encrypted_jwt_token( username=users[3].username, sensitive_content="password", ) share_project_private_key( from_user=users[3], to_another=users[6], from_user_token=user3_token, project=projects[3], ) db.session.commit()
def test_user_key_not_found_error_for_project(client): project_without_keys = models.Project( public_id="random_project_id", title="random project_title", description="This is a random project. ", pi="PI", bucket= f"publicproj-{str(timestamp(ts_format='%Y%m%d%H%M%S'))}-{str(uuid.uuid4())}", ) # Somehow the key pair for the project is not created or persisted to the database invite1 = models.Invite(email="*****@*****.**", role="Unit Personnel") unituser = models.User.query.filter_by(username="******").first() unituser.unit.projects.append(project_without_keys) dds_web.db.session.commit() unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content="password", ) with pytest.raises(KeyNotFoundError) as error: share_project_private_key( from_user=unituser, to_another=invite1, from_user_token=unituser_token, project=project_without_keys, ) assert "Unrecoverable key error. Aborting." in str(error.value)
def get_email_token(invite): return encrypted_jwt_token( username="", sensitive_content=generate_invite_key_pair(invite).hex(), expires_in=datetime.timedelta(hours=24), additional_claims={"inv": invite.email}, )
def get_valid_reset_token(username, expires_in=3600): return encrypted_jwt_token( username=username, sensitive_content=None, expires_in=datetime.timedelta(seconds=expires_in, ), additional_claims={"rst": "pwd"}, )
def test_auth_second_factor_incorrect_token(client): """ Test that the two_factor endpoint called with a password_reset token returns 401/UNAUTHORIZED and does not send a mail. """ user_auth = tests.UserAuth(tests.USER_CREDENTIALS["researcher"]) hotp_token = user_auth.fetch_hotp() reset_token = encrypted_jwt_token( username="******", sensitive_content=None, expires_in=datetime.timedelta(seconds=3600, ), additional_claims={"rst": "pwd"}, ) response = client.get( tests.DDSEndpoint.SECOND_FACTOR, headers={"Authorization": f"Bearer {reset_token}"}, json={"HOTP": hotp_token.decode()}, ) assert response.status_code == http.HTTPStatus.UNAUTHORIZED response_json = response.json assert response_json.get("message") assert "Invalid token" == response_json.get("message")
def test_expired_encrypted_token(client): token = encrypted_jwt_token( username="******", sensitive_content=None, expires_in=datetime.timedelta(seconds=-2) ) with pytest.raises(AuthenticationError) as error: verify_token(token) assert "Expired token" in str(error.value)
def get(self): return { "message": "Please take this token to /user/second_factor to authenticate with MFA!", "token": encrypted_jwt_token( username=auth.current_user().username, sensitive_content=flask.request.authorization.get("password"), ), }
def test_invalid_invite_token_without_an_email(client): with pytest.raises(AuthenticationError) as error: verify_invite_token( encrypted_jwt_token( username="", sensitive_content="bogus", expires_in=datetime.timedelta(hours=1), ) ) assert "Invalid token" in str(error.value)
def test_exp_for_cli_not_in_protected_header_of_reset_password_token(client): token = encrypted_jwt_token( username="******", sensitive_content=None, expires_in=datetime.timedelta( seconds=3600, ), additional_claims={"rst": "pwd"}, ) token = jwt.JWT(jwt=token) assert "exp" not in token.token.jose_header
def test_exp_for_cli_not_in_protected_header_of_invite_token(client): token = encrypted_jwt_token( username="", sensitive_content=b"bogus".hex(), expires_in=datetime.timedelta( hours=flask.current_app.config["INVITATION_EXPIRES_IN_HOURS"] ), additional_claims={"inv": "*****@*****.**"}, ) token = jwt.JWT(jwt=token) assert "exp" not in token.token.jose_header
def test_extract_token_invite_key_with_no_invite(client): with pytest.raises(InviteError) as error: extract_token_invite_key( encrypted_jwt_token( username="", sensitive_content="bogus", expires_in=datetime.timedelta(hours=24), additional_claims={"inv": "*****@*****.**"}, ) ) assert "Invite could not be found!" in str(error.value)
def test_invalid_invite_token_with_a_sub(client): with pytest.raises(AuthenticationError) as error: verify_invite_token( encrypted_jwt_token( username="******", sensitive_content="bogus", expires_in=datetime.timedelta(hours=1), additional_claims={"inv": "*****@*****.**"}, ) ) assert "Invalid token" in str(error.value)
def test_valid_invite_token_with_absent_invite(client): email, invite_row = verify_invite_token( encrypted_jwt_token( username="", sensitive_content="bogus", expires_in=datetime.timedelta(hours=24), additional_claims={"inv": "*****@*****.**"}, ) ) assert email == "*****@*****.**" assert not invite_row
def test_valid_invite_token(client): email, invite_row = verify_invite_token( encrypted_jwt_token( username="", sensitive_content="bogus", expires_in=datetime.timedelta(hours=24), additional_claims={"inv": "*****@*****.**"}, ) ) assert email == "*****@*****.**" assert invite_row assert invite_row.email == "*****@*****.**"
def test_extract_token_invite_key_successful(client): invite, temporary_key = extract_token_invite_key( encrypted_jwt_token( username="", sensitive_content=b"bogus".hex(), expires_in=datetime.timedelta(hours=24), additional_claims={"inv": "*****@*****.**"}, ) ) assert invite assert invite.email == "*****@*****.**" assert temporary_key == b"bogus"
def test_invite_key_verification_fails_with_wrong_invalid_key(client): invite = models.Invite.query.filter_by( email="*****@*****.**", role="Researcher").one_or_none() assert invite generate_invite_key_pair(invite) assert invite.nonce is not None assert invite.private_key is not None assert invite.public_key is not None token = encrypted_jwt_token( username="", sensitive_content=b"wrong_key".hex(), expires_in=datetime.timedelta(hours=24), additional_claims={"inv": invite.email}, ) assert token response = client.get(tests.DDSEndpoint.USER_CONFIRM + token, content_type="application/json") assert response.status == "200 OK" assert b"Create account" in response.data form_token = flask.g.csrf_token form_data = { "csrf_token": form_token, "email": invite.email, "name": "Test User", "username": "******", "password": "******", "confirm": "Password123", "submit": "submit", } response = client.post( tests.DDSEndpoint.USER_NEW, json=form_data, follow_redirects=True, ) assert response.status == "200 OK" invite = models.Invite.query.filter_by( email="*****@*****.**", role="Researcher").one_or_none() assert invite is not None user = models.User.query.filter_by( username=form_data["username"]).one_or_none() assert user is None
def test_matching_form_email_with_invite_token(client): assert ( matching_email_with_invite( encrypted_jwt_token( username="", sensitive_content="bogus", expires_in=datetime.timedelta(hours=24), additional_claims={"inv": "*****@*****.**"}, ), "*****@*****.**", ) is True )
def test_extract_token_invite_key_with_wrong_format_for_key(client): with pytest.raises(ValueError) as error: extract_token_invite_key( encrypted_jwt_token( username="", sensitive_content="bogus", expires_in=datetime.timedelta(hours=24), additional_claims={"inv": "*****@*****.**"}, ) ) assert "Temporary key is expected be in hexadecimal digits for a byte string." in str( error.value )
def test_encrypted_and_signed_token(client): username = "******" expires_in = datetime.timedelta(minutes=1) expiry_datetime = dds_web.utils.current_time() + expires_in additional_claim = {"iss": "DDS"} encrypted_token = encrypted_jwt_token( username=username, sensitive_content=None, expires_in=expires_in, additional_claims=additional_claim, ) token_content = decrypt_and_verify_token_signature(encrypted_token) assert username == token_content.get("sub") assert "DDS" == token_content.get("iss") token_expiry_datetime = datetime.datetime.fromtimestamp(token_content.get("exp")) assert token_expiry_datetime - expiry_datetime < datetime.timedelta(seconds=1)
def test_user_key_operation_error_with_decrypt_user_private_key(client): invite1 = models.Invite(email="*****@*****.**", role="Unit Personnel") unituser = models.User.query.filter_by(username="******").first() # Somehow a wrong password has ended up in the encrypted token unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content="passwor", ) with pytest.raises(KeyOperationError) as error: share_project_private_key( from_user=unituser, to_another=invite1, from_user_token=unituser_token, project=unituser.unit.projects[0], ) assert "User private key could not be decrypted!" in str(error.value)
def test_user_key_setup_error_with_public_key(client): invite1 = models.Invite(email="*****@*****.**", role="Unit Personnel") # Somehow the key pair for the invite has not taken place or disappeared unituser = models.User.query.filter_by(username="******").first() unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content="password", ) with pytest.raises(KeySetupError) as error: share_project_private_key( from_user=unituser, to_another=invite1, from_user_token=unituser_token, project=unituser.unit.projects[0], ) assert "User keys are not properly setup!" in str(error.value)
def test_sensitive_content_missing_error(client): invite1 = models.Invite(email="*****@*****.**", role="Unit Personnel") unituser = models.User.query.filter_by(username="******").first() # Somehow the password is missing in the encrypted token unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content=None, ) with pytest.raises(SensitiveContentMissingError) as error: share_project_private_key( from_user=unituser, to_another=invite1, from_user_token=unituser_token, project=unituser.unit.projects[0], ) assert "Sensitive content is missing in the encrypted token!" in str( error.value)
def perform_invite(client, inviting_user, email, role=None, project=None): json_data = {"email": email, "role": role} query_string = {} if project: if not role: raise ValueError( "Role must be specified when inviting to a project") query_string = {"project": project.public_id} # get the auth token here to avoid interfering with the invite token fetching auth_token = tests.UserAuth( tests.USER_CREDENTIALS[inviting_user.username]).token(client) # Need to get hold of the actual invite token invite_token = None with unittest.mock.patch.object(dds_web.api.user, "encrypted_jwt_token", return_value="token") as mock_token_method: response = client.post( tests.DDSEndpoint.USER_ADD, headers=auth_token, query_string=query_string, json=json_data, content_type="application/json", ) if DEBUG: print(response.data) # New invite token is not generated if invite is already sent assert mock_token_method.call_count <= 1 if mock_token_method.call_args is not None: call_args = mock_token_method.call_args invite_token = encrypted_jwt_token(*call_args.args, **call_args.kwargs) if response.status != "200 OK": if DEBUG: print(response.status_code) raise ValueError(f"Invitation failed: {response.data}") return invite_token
def test_user_key_operation_error_with_load_user_public_key(client): invite1 = models.Invite(email="*****@*****.**", role="Unit Personnel") generate_invite_key_pair(invite1) unituser = models.User.query.filter_by(username="******").first() unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content="password", ) # Somehow the public key of the invite is not the expected public key invite1.public_key = b"useless_bytes" with pytest.raises(KeyOperationError) as error: share_project_private_key( from_user=unituser, to_another=invite1, from_user_token=unituser_token, project=unituser.unit.projects[0], ) assert "User public key could not be loaded!" in str(error.value)
def invite_user(email, new_user_role, project=None, unit=None): """Invite a new user""" current_user_role = get_user_roles_common(user=auth.current_user()) if not project: if current_user_role == "Project Owner": return { "status": ddserr.InviteError.code.value, "message": "Project ID required to invite users to projects.", } if new_user_role == "Project Owner": return { "status": ddserr.InviteError.code.value, "message": "Project ID required to invite a 'Project Owner'.", } # Verify role or current and new user if current_user_role == "Super Admin" and project: return { "status": ddserr.InviteError.code.value, "message": ("Super Admins do not have project data access and can therefore " "not invite users to specific projects."), } elif current_user_role == "Unit Admin" and new_user_role == "Super Admin": return { "status": ddserr.AccessDeniedError.code.value, "message": ddserr.AccessDeniedError.description, } elif current_user_role == "Unit Personnel" and new_user_role in [ "Super Admin", "Unit Admin", ]: return { "status": ddserr.AccessDeniedError.code.value, "message": ddserr.AccessDeniedError.description, } elif current_user_role == "Project Owner" and new_user_role in [ "Super Admin", "Unit Admin", "Unit Personnel", ]: return { "status": ddserr.AccessDeniedError.code.value, "message": ddserr.AccessDeniedError.description, } elif current_user_role == "Researcher": return { "status": ddserr.AccessDeniedError.code.value, "message": ddserr.AccessDeniedError.description, } # Create invite row new_invite = models.Invite( email=email, role=("Researcher" if new_user_role == "Project Owner" else new_user_role), ) # Create URL safe token for invitation link token = encrypted_jwt_token( username="", sensitive_content=generate_invite_key_pair( invite=new_invite).hex(), expires_in=datetime.timedelta( hours=flask.current_app.config["INVITATION_EXPIRES_IN_HOURS"]), additional_claims={"inv": new_invite.email}, ) # Create link for invitation email link = flask.url_for("auth_blueprint.confirm_invite", token=token, _external=True) # Quick search gave this as the URL length limit. if len(link) >= 2048: flask.current_app.logger.error( "Invitation link was not possible to create due to length.") return { "message": "Invite failed due to server error", "status": http.HTTPStatus.INTERNAL_SERVER_ERROR, } projects_not_shared = {} goahead = False # Append invite to unit if applicable if new_invite.role in ["Unit Admin", "Unit Personnel"]: # TODO Change / move this later. This is just so that we can add an initial Unit Admin. if auth.current_user().role == "Super Admin": if unit: unit_row = models.Unit.query.filter_by( public_id=unit).one_or_none() if not unit_row: raise ddserr.DDSArgumentError( message="Invalid unit publid id.") unit_row.invites.append(new_invite) goahead = True else: raise ddserr.DDSArgumentError( message= "You need to specify a unit to invite a Unit Personnel or Unit Admin." ) if "Unit" in auth.current_user().role: # Give new unit user access to all projects of the unit auth.current_user().unit.invites.append(new_invite) if auth.current_user().unit.projects: for unit_project in auth.current_user().unit.projects: if unit_project.is_active: try: share_project_private_key( from_user=auth.current_user(), to_another=new_invite, from_user_token=dds_web.security.auth. obtain_current_encrypted_token(), project=unit_project, ) except ddserr.KeyNotFoundError as keyerr: projects_not_shared[ unit_project. public_id] = "You do not have access to the project(s)" else: goahead = True else: goahead = True if not project: # specified project is disregarded for unituser invites msg = f"{str(new_invite)} was successful." else: msg = f"{str(new_invite)} was successful, but specification for {str(project)} dropped. Unit Users have automatic access to projects of their unit." else: db.session.add(new_invite) if project: try: share_project_private_key( from_user=auth.current_user(), to_another=new_invite, project=project, from_user_token=dds_web.security.auth. obtain_current_encrypted_token(), is_project_owner=new_user_role == "Project Owner", ) except ddserr.KeyNotFoundError as keyerr: projects_not_shared[ project. public_id] = "You do not have access to the specified project." else: goahead = True else: goahead = True # Compose and send email status_code = http.HTTPStatus.OK if goahead: try: db.session.commit() except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.OperationalError) as sqlerr: db.session.rollback() raise ddserr.DatabaseError( message=str(sqlerr), alt_message=f"Invitation failed" + (": Database malfunction." if isinstance( sqlerr, sqlalchemy.exc.OperationalError) else "."), ) from sqlerr AddUser.compose_and_send_email_to_user(userobj=new_invite, mail_type="invite", link=link) msg = f"{str(new_invite)} was successful." else: msg = (f"The user could not be added to the project(s)." if projects_not_shared else "Unknown error!") + " The invite did not succeed." status_code = ddserr.InviteError.code.value return { "email": new_invite.email, "message": msg, "status": status_code, "errors": projects_not_shared, }
def test_exp_for_cli_not_in_protected_header_of_partial_token(client): token = encrypted_jwt_token(username="******", sensitive_content=None) token = jwt.JWT(jwt=token) assert "exp" not in token.token.jose_header
def test_encrypted_data_destined_for_another_user(client): encrypted_token = encrypted_jwt_token( username="******", sensitive_content="sensitive_content" ) extracted_content = extract_encrypted_token_sensitive_content(encrypted_token, "projectowner") assert extracted_content is None
def test_share_project_keys_via_two_invites(client): # this test focuses only on the secure parts related to the following scenario # unituser invites a new Unit Personnel invite1 = models.Invite(email="*****@*****.**", role="Unit Personnel") temporary_key = generate_invite_key_pair(invite1) invite_token1 = encrypted_jwt_token( username="", sensitive_content=temporary_key.hex(), additional_claims={"inv": invite1.email}, ) unituser = models.User.query.filter_by(username="******").first() unituser.unit.invites.append(invite1) unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content="password", ) for project in unituser.unit.projects: share_project_private_key( from_user=unituser, to_another=invite1, from_user_token=unituser_token, project=project, ) dds_web.db.session.commit() # ************************************ # invited Unit Personnel follows the link and registers itself common_user_fields = { "username": "******", "password": "******", "name": "Test User", } new_user = models.UnitUser(**common_user_fields) invite1.unit.users.append(new_user) new_email = models.Email(email=invite1.email, primary=True) new_user.emails.append(new_email) new_user.active = True dds_web.db.session.add(new_user) verify_and_transfer_invite_to_user(invite_token1, new_user, common_user_fields["password"]) for project_invite_key in invite1.project_invite_keys: project_user_key = models.ProjectUserKeys( project_id=project_invite_key.project_id, user_id=new_user.username, key=project_invite_key.key, ) dds_web.db.session.add(project_user_key) dds_web.db.session.delete(project_invite_key) assert invite1.nonce != new_user.nonce assert invite1.public_key == new_user.public_key assert invite1.private_key != new_user.private_key dds_web.db.session.delete(invite1) dds_web.db.session.commit() # ************************************ # new Unit Personnel invites another new Unit Personnel invite2 = models.Invite(email="*****@*****.**", role="Unit Personnel") invite_token2 = encrypted_jwt_token( username="", sensitive_content=generate_invite_key_pair(invite2).hex(), additional_claims={"inv": invite2.email}, ) unituser = models.User.query.filter_by( username="******").first() unituser.unit.invites.append(invite2) unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content=common_user_fields["password"], ) for project in unituser.unit.projects: share_project_private_key( from_user=unituser, to_another=invite2, from_user_token=unituser_token, project=project, ) dds_web.db.session.commit() project_invite_keys = invite2.project_invite_keys number_of_asserted_projects = 0 for project_invite_key in project_invite_keys: if (project_invite_key.project.public_id == "public_project_id" or project_invite_key.project.public_id == "unused_project_id" or project_invite_key.project.public_id == "restricted_project_id" or project_invite_key.project.public_id == "second_public_project_id" or project_invite_key.project.public_id == "file_testing_project"): number_of_asserted_projects += 1 assert len(project_invite_keys) == number_of_asserted_projects assert len(project_invite_keys) == 5
def test_password_reset(client: flask.testing.FlaskClient): user_auth: UserAuth = UserAuth(USER_CREDENTIALS["researcher"]) successful_web_login(client, user_auth) headers: Dict = user_auth.token(client) token: str = encrypted_jwt_token( username="******", sensitive_content=b"".hex(), expires_in=datetime.timedelta(hours=24), additional_claims={"inv": "researchuser", "rst": "pwd"}, ) response: werkzeug.test.WrapperTestResponse = client.get( DDSEndpoint.USER_INFO, headers=headers, follow_redirects=True ) assert response.status_code == HTTPStatus.OK assert flask.request.path == DDSEndpoint.USER_INFO form_token: str = flask.g.csrf_token response: werkzeug.test.WrapperTestResponse = client.post( DDSEndpoint.LOGOUT, follow_redirects=True, ) assert response.status_code == HTTPStatus.OK assert flask.request.path == DDSEndpoint.INDEX response: werkzeug.test.WrapperTestResponse = client.post( DDSEndpoint.REQUEST_RESET_PASSWORD, headers={"Content-Type": "application/x-www-form-urlencoded"}, data={ "csrf_token": form_token, "email": "*****@*****.**", }, follow_redirects=True, ) assert response.status_code == HTTPStatus.OK assert response.content_type == "text/html; charset=utf-8" assert flask.request.path == DDSEndpoint.LOGIN response: werkzeug.test.WrapperTestResponse = client.post( f"{DDSEndpoint.REQUEST_RESET_PASSWORD}/{token}", headers={"Content-Type": "application/x-www-form-urlencoded"}, data={ "csrf_token": form_token, "password": "******", "confirm_password": "******", }, follow_redirects=True, ) assert response.status_code == HTTPStatus.OK assert response.content_type == "text/html; charset=utf-8" assert flask.request.path == DDSEndpoint.PASSWORD_RESET_COMPLETED with client.session_transaction() as session: session["reset_token"] = token response: werkzeug.test.WrapperTestResponse = client.get( DDSEndpoint.PASSWORD_RESET_COMPLETED, follow_redirects=True, ) assert response.status_code == HTTPStatus.OK assert response.content_type == "text/html; charset=utf-8" assert flask.request.path == DDSEndpoint.PASSWORD_RESET_COMPLETED response: werkzeug.test.WrapperTestResponse = client.get( DDSEndpoint.USER_INFO, headers=headers, ) assert response.status_code == HTTPStatus.UNAUTHORIZED assert response.content_type == "application/json" assert flask.request.path == DDSEndpoint.USER_INFO assert ( response.json.get("message") == "Password reset performed after last authentication. Start a new authenticated session to proceed." )