selfprivacy-rest-api/tests/test_rest_endpoints/test_auth.py

455 lines
14 KiB
Python
Raw Permalink Normal View History

2022-01-18 15:20:47 +00:00
# pylint: disable=redefined-outer-name
# pylint: disable=unused-argument
2022-06-29 17:39:46 +00:00
# pylint: disable=missing-function-docstring
2022-01-24 20:01:37 +00:00
import datetime
2022-01-18 15:20:47 +00:00
import pytest
2022-06-24 18:14:20 +00:00
from tests.conftest import TOKENS_FILE_CONTENTS
from tests.common import (
RECOVERY_KEY_VALIDATION_DATETIME,
DEVICE_KEY_VALIDATION_DATETIME,
NearFuture,
2023-01-06 10:59:59 +00:00
assert_recovery_recent,
)
from tests.common import FIVE_MINUTES_INTO_FUTURE_NAIVE as FIVE_MINUTES_INTO_FUTURE
from tests.common import FIVE_MINUTES_INTO_PAST_NAIVE as FIVE_MINUTES_INTO_PAST
2022-01-18 15:20:47 +00:00
2022-07-08 15:28:08 +00:00
DATE_FORMATS = [
"%Y-%m-%dT%H:%M:%S.%fZ",
"%Y-%m-%dT%H:%M:%S.%f",
"%Y-%m-%d %H:%M:%S.%fZ",
"%Y-%m-%d %H:%M:%S.%f",
]
def assert_original(client):
new_tokens = rest_get_tokens_info(client)
for token in TOKENS_FILE_CONTENTS["tokens"]:
assert_token_valid(client, token["token"])
for new_token in new_tokens:
if new_token["name"] == token["name"]:
assert (
datetime.datetime.fromisoformat(new_token["date"]) == token["date"]
)
assert_no_recovery(client)
def assert_token_valid(client, token):
client.headers.update({"Authorization": "Bearer " + token})
assert rest_get_tokens_info(client) is not None
def rest_get_tokens_info(client):
response = client.get("/auth/tokens")
2022-01-18 15:20:47 +00:00
assert response.status_code == 200
return response.json()
def rest_try_authorize_new_device(client, token, device_name):
response = client.post(
"/auth/new_device/authorize",
json={
"token": token,
"device": device_name,
},
)
return response
def rest_make_recovery_token(client, expires_at=None, timeformat=None, uses=None):
json = {}
if expires_at is not None:
assert timeformat is not None
expires_at_str = expires_at.strftime(timeformat)
json["expiration"] = expires_at_str
if uses is not None:
json["uses"] = uses
if json == {}:
response = client.post("/auth/recovery_token")
else:
response = client.post(
"/auth/recovery_token",
json=json,
)
assert response.status_code == 200
assert "token" in response.json()
return response.json()["token"]
def rest_get_recovery_status(client):
response = client.get("/auth/recovery_token")
assert response.status_code == 200
return response.json()
def rest_get_recovery_date(client):
status = rest_get_recovery_status(client)
assert "date" in status
return status["date"]
def assert_no_recovery(client):
assert not rest_get_recovery_status(client)["exists"]
def rest_recover_with_mnemonic(client, mnemonic_token, device_name):
recovery_response = client.post(
"/auth/recovery_token/use",
json={"token": mnemonic_token, "device": device_name},
)
assert recovery_response.status_code == 200
new_token = recovery_response.json()["token"]
assert_token_valid(client, new_token)
return new_token
# Tokens
def test_get_tokens_info(authorized_client, tokens_file):
assert sorted(rest_get_tokens_info(authorized_client), key=lambda x: x["name"]) == [
{"name": "test_token", "date": "2022-01-14T08:31:10.789314", "is_caller": True},
{
"name": "test_token2",
"date": "2022-01-14T08:31:10.789314",
"is_caller": False,
},
2022-01-18 15:20:47 +00:00
]
def test_get_tokens_unauthorized(client, tokens_file):
response = client.get("/auth/tokens")
assert response.status_code == 401
def test_delete_token_unauthorized(client, authorized_client, tokens_file):
2022-01-18 15:20:47 +00:00
response = client.delete("/auth/tokens")
assert response.status_code == 401
assert_original(authorized_client)
2022-01-18 15:20:47 +00:00
def test_delete_token(authorized_client, tokens_file):
response = authorized_client.delete(
"/auth/tokens", json={"token_name": "test_token2"}
)
assert response.status_code == 200
assert rest_get_tokens_info(authorized_client) == [
{"name": "test_token", "date": "2022-01-14T08:31:10.789314", "is_caller": True}
]
2022-01-18 15:20:47 +00:00
2022-01-24 20:01:37 +00:00
2022-01-18 15:20:47 +00:00
def test_delete_self_token(authorized_client, tokens_file):
response = authorized_client.delete(
"/auth/tokens", json={"token_name": "test_token"}
)
assert response.status_code == 400
assert_original(authorized_client)
2022-01-18 15:20:47 +00:00
2022-01-24 20:01:37 +00:00
2022-01-18 15:20:47 +00:00
def test_delete_nonexistent_token(authorized_client, tokens_file):
response = authorized_client.delete(
"/auth/tokens", json={"token_name": "test_token3"}
)
assert response.status_code == 404
assert_original(authorized_client)
2022-01-18 15:20:47 +00:00
2022-01-24 20:01:37 +00:00
def test_refresh_token_unauthorized(client, authorized_client, tokens_file):
2022-01-18 15:20:47 +00:00
response = client.post("/auth/tokens")
assert response.status_code == 401
assert_original(authorized_client)
2022-01-18 15:20:47 +00:00
2022-01-24 20:01:37 +00:00
2022-01-18 15:20:47 +00:00
def test_refresh_token(authorized_client, tokens_file):
response = authorized_client.post("/auth/tokens")
assert response.status_code == 200
new_token = response.json()["token"]
assert_token_valid(authorized_client, new_token)
2022-01-24 20:01:37 +00:00
# New device
2022-01-24 20:01:37 +00:00
def test_get_new_device_auth_token_unauthorized(client, authorized_client, tokens_file):
response = client.post("/auth/new_device")
2022-01-24 20:01:37 +00:00
assert response.status_code == 401
assert "token" not in response.json()
assert "detail" in response.json()
# We only can check existence of a token we know.
2022-01-24 20:01:37 +00:00
def test_get_and_delete_new_device_token(client, authorized_client, tokens_file):
token = rest_get_new_device_token(authorized_client)
response = authorized_client.delete("/auth/new_device", json={"token": token})
2022-01-27 12:12:49 +00:00
assert response.status_code == 200
assert rest_try_authorize_new_device(client, token, "new_device").status_code == 404
2022-01-27 12:12:49 +00:00
def test_delete_token_unauthenticated(client, authorized_client, tokens_file):
token = rest_get_new_device_token(authorized_client)
response = client.delete("/auth/new_device", json={"token": token})
2022-01-27 12:12:49 +00:00
assert response.status_code == 401
assert rest_try_authorize_new_device(client, token, "new_device").status_code == 200
2022-01-27 12:12:49 +00:00
def rest_get_new_device_token(client):
response = client.post("/auth/new_device")
2022-01-24 20:01:37 +00:00
assert response.status_code == 200
assert "token" in response.json()
return response.json()["token"]
def test_get_and_authorize_new_device(client, authorized_client, tokens_file):
token = rest_get_new_device_token(authorized_client)
response = rest_try_authorize_new_device(client, token, "new_device")
2022-01-24 20:01:37 +00:00
assert response.status_code == 200
assert_token_valid(authorized_client, response.json()["token"])
2022-01-24 20:01:37 +00:00
def test_authorize_new_device_with_invalid_token(
client, authorized_client, tokens_file
):
response = rest_try_authorize_new_device(client, "invalid_token", "new_device")
2022-01-24 20:01:37 +00:00
assert response.status_code == 404
assert_original(authorized_client)
2022-01-24 20:01:37 +00:00
def test_get_and_authorize_used_token(client, authorized_client, tokens_file):
token_to_be_used_2_times = rest_get_new_device_token(authorized_client)
response = rest_try_authorize_new_device(
client, token_to_be_used_2_times, "new_device"
2022-01-24 20:01:37 +00:00
)
assert response.status_code == 200
assert_token_valid(authorized_client, response.json()["token"])
response = rest_try_authorize_new_device(
client, token_to_be_used_2_times, "new_device"
2022-01-24 20:01:37 +00:00
)
assert response.status_code == 404
def test_get_and_authorize_token_after_12_minutes(
client, authorized_client, tokens_file, mocker
2022-01-24 20:01:37 +00:00
):
token = rest_get_new_device_token(authorized_client)
2022-01-24 20:01:37 +00:00
# TARDIS sounds
mock = mocker.patch(DEVICE_KEY_VALIDATION_DATETIME, NearFuture)
2022-01-24 20:01:37 +00:00
response = rest_try_authorize_new_device(client, token, "new_device")
2022-01-24 20:01:37 +00:00
assert response.status_code == 404
assert_original(authorized_client)
2022-01-24 20:01:37 +00:00
def test_authorize_without_token(client, authorized_client, tokens_file):
2022-01-24 20:01:37 +00:00
response = client.post(
"/auth/new_device/authorize",
json={"device": "new_device"},
)
assert response.status_code == 422
assert_original(authorized_client)
2022-01-24 20:01:37 +00:00
# Recovery tokens
# GET /auth/recovery_token returns token status
# - if token is valid, returns 200 and token status
# - token status:
# - exists (boolean)
# - valid (boolean)
# - date (string)
# - expiration (string)
# - uses_left (int)
# - if token is invalid, returns 400 and empty body
# POST /auth/recovery_token generates a new token
# has two optional parameters:
# - expiration (string in datetime format)
# - uses_left (int)
# POST /auth/recovery_token/use uses the token
# required arguments:
# - token (string)
# - device (string)
# - if token is valid, returns 200 and token
# - if token is invalid, returns 404
# - if request is invalid, returns 400
def test_get_recovery_token_status_unauthorized(client, authorized_client, tokens_file):
2022-01-24 20:01:37 +00:00
response = client.get("/auth/recovery_token")
assert response.status_code == 401
assert_original(authorized_client)
2022-01-24 20:01:37 +00:00
2022-02-16 12:49:10 +00:00
def test_get_recovery_token_when_none_exists(authorized_client, tokens_file):
response = authorized_client.get("/auth/recovery_token")
assert response.status_code == 200
assert response.json() == {
2022-02-16 12:49:10 +00:00
"exists": False,
"valid": False,
"date": None,
"expiration": None,
"uses_left": None,
}
assert_original(authorized_client)
2022-02-16 12:49:10 +00:00
def test_generate_recovery_token(authorized_client, client, tokens_file):
# Generate token without expiration and uses_left
mnemonic_token = rest_make_recovery_token(authorized_client)
time_generated = rest_get_recovery_date(authorized_client)
assert_recovery_recent(time_generated)
assert rest_get_recovery_status(authorized_client) == {
2022-02-16 12:49:10 +00:00
"exists": True,
"valid": True,
"date": time_generated,
"expiration": None,
"uses_left": None,
}
rest_recover_with_mnemonic(client, mnemonic_token, "recover_device")
# And again
rest_recover_with_mnemonic(client, mnemonic_token, "recover_device2")
2022-02-16 12:49:10 +00:00
2022-07-08 15:28:08 +00:00
@pytest.mark.parametrize("timeformat", DATE_FORMATS)
2022-02-16 12:49:10 +00:00
def test_generate_recovery_token_with_expiration_date(
authorized_client, client, tokens_file, timeformat, mocker
2022-02-16 12:49:10 +00:00
):
# Generate token with expiration date
# Generate expiration date in the future
expiration_date = FIVE_MINUTES_INTO_FUTURE
mnemonic_token = rest_make_recovery_token(
authorized_client, expires_at=expiration_date, timeformat=timeformat
2022-02-16 12:49:10 +00:00
)
time_generated = rest_get_recovery_date(authorized_client)
assert_recovery_recent(time_generated)
assert rest_get_recovery_status(authorized_client) == {
2022-02-16 12:49:10 +00:00
"exists": True,
"valid": True,
"date": time_generated,
"expiration": expiration_date.isoformat(),
2022-02-16 12:49:10 +00:00
"uses_left": None,
}
rest_recover_with_mnemonic(client, mnemonic_token, "recover_device")
# And again
rest_recover_with_mnemonic(client, mnemonic_token, "recover_device2")
2022-02-16 12:49:10 +00:00
# Try to use token after expiration date
mock = mocker.patch(RECOVERY_KEY_VALIDATION_DATETIME, NearFuture)
device_name = "recovery_device3"
2022-02-16 12:49:10 +00:00
recovery_response = client.post(
"/auth/recovery_token/use",
json={"token": mnemonic_token, "device": device_name},
2022-02-16 12:49:10 +00:00
)
assert recovery_response.status_code == 404
# Assert that the token was not created
assert device_name not in [
token["name"] for token in rest_get_tokens_info(authorized_client)
]
2022-02-16 12:49:10 +00:00
2022-07-08 15:28:08 +00:00
@pytest.mark.parametrize("timeformat", DATE_FORMATS)
2022-02-16 12:49:10 +00:00
def test_generate_recovery_token_with_expiration_in_the_past(
2022-07-08 15:28:08 +00:00
authorized_client, tokens_file, timeformat
2022-02-16 12:49:10 +00:00
):
# Server must return 400 if expiration date is in the past
expiration_date = FIVE_MINUTES_INTO_PAST
2022-07-08 15:28:08 +00:00
expiration_date_str = expiration_date.strftime(timeformat)
2022-02-16 12:49:10 +00:00
response = authorized_client.post(
"/auth/recovery_token",
json={"expiration": expiration_date_str},
)
assert response.status_code == 400
assert_no_recovery(authorized_client)
2022-02-16 12:49:10 +00:00
def test_generate_recovery_token_with_invalid_time_format(
2022-06-29 17:39:46 +00:00
authorized_client, tokens_file
2022-02-16 12:49:10 +00:00
):
# Server must return 400 if expiration date is in the past
expiration_date = "invalid_time_format"
response = authorized_client.post(
"/auth/recovery_token",
json={"expiration": expiration_date},
)
assert response.status_code == 422
assert_no_recovery(authorized_client)
2022-02-16 12:49:10 +00:00
def test_generate_recovery_token_with_limited_uses(
authorized_client, client, tokens_file
):
# Generate token with limited uses
mnemonic_token = rest_make_recovery_token(authorized_client, uses=2)
2022-02-16 12:49:10 +00:00
time_generated = rest_get_recovery_date(authorized_client)
assert_recovery_recent(time_generated)
assert rest_get_recovery_status(authorized_client) == {
2022-02-16 12:49:10 +00:00
"exists": True,
"valid": True,
"date": time_generated,
"expiration": None,
"uses_left": 2,
}
# Try to use the token
rest_recover_with_mnemonic(client, mnemonic_token, "recover_device")
2022-02-16 12:49:10 +00:00
assert rest_get_recovery_status(authorized_client) == {
2022-02-16 12:49:10 +00:00
"exists": True,
"valid": True,
"date": time_generated,
"expiration": None,
"uses_left": 1,
}
# Try to use token again
rest_recover_with_mnemonic(client, mnemonic_token, "recover_device2")
2022-02-16 12:49:10 +00:00
assert rest_get_recovery_status(authorized_client) == {
2022-02-16 12:49:10 +00:00
"exists": True,
"valid": False,
"date": time_generated,
"expiration": None,
"uses_left": 0,
}
# Try to use token after limited uses
recovery_response = client.post(
"/auth/recovery_token/use",
json={"token": mnemonic_token, "device": "recovery_device3"},
)
assert recovery_response.status_code == 404
2022-02-16 14:10:45 +00:00
def test_generate_recovery_token_with_negative_uses(
authorized_client, client, tokens_file
):
# Generate token with limited uses
response = authorized_client.post(
"/auth/recovery_token",
json={"uses": -2},
)
assert response.status_code == 400
assert_no_recovery(authorized_client)
2022-02-16 14:10:45 +00:00
def test_generate_recovery_token_with_zero_uses(authorized_client, client, tokens_file):
# Generate token with limited uses
response = authorized_client.post(
"/auth/recovery_token",
json={"uses": 0},
)
assert response.status_code == 400
assert_no_recovery(authorized_client)