Exemplo n.º 1
0
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"
Exemplo n.º 2
0
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()
Exemplo n.º 3
0
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},
    )
Exemplo n.º 5
0
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"},
    )
Exemplo n.º 6
0
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")
Exemplo n.º 7
0
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)
Exemplo n.º 8
0
 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"),
         ),
     }
Exemplo n.º 9
0
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)
Exemplo n.º 10
0
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
Exemplo n.º 11
0
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
Exemplo n.º 12
0
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)
Exemplo n.º 13
0
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)
Exemplo n.º 14
0
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
Exemplo n.º 15
0
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 == "*****@*****.**"
Exemplo n.º 16
0
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
Exemplo n.º 18
0
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
    )
Exemplo n.º 19
0
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
    )
Exemplo n.º 20
0
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)
Exemplo n.º 21
0
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)
Exemplo n.º 22
0
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)
Exemplo n.º 23
0
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
Exemplo n.º 25
0
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)
Exemplo n.º 26
0
    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,
        }
Exemplo n.º 27
0
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
Exemplo n.º 28
0
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
Exemplo n.º 29
0
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
Exemplo n.º 30
0
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."
    )