Expose more options for Cookie authentication

This commit is contained in:
François Voron
2020-01-11 10:55:02 +01:00
parent c537b58d52
commit dbbb9144b0
4 changed files with 77 additions and 21 deletions

View File

@ -11,6 +11,7 @@ format: isort-src isort-docs
$(PIPENV_RUN) black . $(PIPENV_RUN) black .
test: test:
docker stop $(MONGODB_CONTAINER_NAME) || true
docker run -d --rm --name $(MONGODB_CONTAINER_NAME) -p 27017:27017 mvertes/alpine-mongo docker run -d --rm --name $(MONGODB_CONTAINER_NAME) -p 27017:27017 mvertes/alpine-mongo
$(PIPENV_RUN) pytest --cov=fastapi_users/ $(PIPENV_RUN) pytest --cov=fastapi_users/
docker stop $(MONGODB_CONTAINER_NAME) docker stop $(MONGODB_CONTAINER_NAME)

View File

@ -18,9 +18,7 @@ auth_backends.append(cookie_authentication)
As you can see, instantiation is quite simple. You just have to define a constant `SECRET` which is used to encode the token and the lifetime of the cookie (in seconds). As you can see, instantiation is quite simple. You just have to define a constant `SECRET` which is used to encode the token and the lifetime of the cookie (in seconds).
You can optionally define the `cookie_name`. **Defaults to `fastapiusersauth`**. You can optionally define the `name` which will be used to generate its [`/login` route](../../usage/routes.md#post-loginname). **Defaults to `cookie`**.
You can also optionally define the `name` which will be used to generate its [`/login` route](../../usage/routes.md#post-loginname). **Defaults to `cookie`**.
```py ```py
cookie_authentication = CookieAuthentication( cookie_authentication = CookieAuthentication(
@ -30,6 +28,14 @@ cookie_authentication = CookieAuthentication(
) )
``` ```
You can also define the parameters for the generated cookie:
* `cookie_name` (`fastapiusersauth`): Name of the cookie.
* `cookie_path` (`/`): Cookie path.
* `cookie_domain` (`None`): Cookie domain.
* `cookie_secure` (`True`): Whether to only send the cookie to the server via SSL request.
* `cookie_httponly` (`True`): Whether to prevent access to the cookie via JavaScript.
!!! tip !!! tip
The value of the cookie is actually a JWT. This authentication backend shares most of its logic with the [JWT](./jwt.md) one. The value of the cookie is actually a JWT. This authentication backend shares most of its logic with the [JWT](./jwt.md) one.

View File

@ -17,22 +17,38 @@ class CookieAuthentication(JWTAuthentication):
:param secret: Secret used to encode the cookie. :param secret: Secret used to encode the cookie.
:param lifetime_seconds: Lifetime duration of the cookie in seconds. :param lifetime_seconds: Lifetime duration of the cookie in seconds.
:param cookie_name: Name of the cookie. :param cookie_name: Name of the cookie.
:param cookie_path: Cookie path.
:param cookie_domain: Cookie domain.
:param cookie_secure: Whether to only send the cookie to the server via SSL request.
:param cookie_httponly: Whether to prevent access to the cookie via JavaScript.
:param name: Name of the backend. It will be used to name the login route. :param name: Name of the backend. It will be used to name the login route.
""" """
lifetime_seconds: int lifetime_seconds: int
cookie_name: str cookie_name: str
cookie_path: str
cookie_domain: Optional[str]
cookie_secure: bool
cookie_httponly: bool
def __init__( def __init__(
self, self,
secret: str, secret: str,
lifetime_seconds: int, lifetime_seconds: int,
cookie_name: str = "fastapiusersauth", cookie_name: str = "fastapiusersauth",
cookie_path: str = "/",
cookie_domain: str = None,
cookie_secure: bool = True,
cookie_httponly: bool = True,
name: str = "cookie", name: str = "cookie",
): ):
super().__init__(secret, lifetime_seconds, name=name) super().__init__(secret, lifetime_seconds, name=name)
self.lifetime_seconds = lifetime_seconds self.lifetime_seconds = lifetime_seconds
self.cookie_name = cookie_name self.cookie_name = cookie_name
self.cookie_path = cookie_path
self.cookie_domain = cookie_domain
self.cookie_secure = cookie_secure
self.cookie_httponly = cookie_httponly
self.api_key_cookie = APIKeyCookie(name=self.cookie_name, auto_error=False) self.api_key_cookie = APIKeyCookie(name=self.cookie_name, auto_error=False)
async def get_login_response(self, user: BaseUserDB, response: Response) -> Any: async def get_login_response(self, user: BaseUserDB, response: Response) -> Any:
@ -41,8 +57,10 @@ class CookieAuthentication(JWTAuthentication):
self.cookie_name, self.cookie_name,
token, token,
max_age=self.lifetime_seconds, max_age=self.lifetime_seconds,
secure=True, path=self.cookie_path,
httponly=True, domain=self.cookie_domain,
secure=self.cookie_secure,
httponly=self.cookie_httponly,
) )
# We shouldn't return directly the response # We shouldn't return directly the response

View File

@ -11,10 +11,19 @@ SECRET = "SECRET"
LIFETIME = 3600 LIFETIME = 3600
COOKIE_NAME = "COOKIE_NAME" COOKIE_NAME = "COOKIE_NAME"
cookie_authentication = CookieAuthentication(SECRET, LIFETIME, COOKIE_NAME)
@pytest.fixture cookie_authentication_path = CookieAuthentication(
def cookie_authentication(): SECRET, LIFETIME, COOKIE_NAME, cookie_path="/arthur"
return CookieAuthentication(SECRET, LIFETIME, COOKIE_NAME) )
cookie_authentication_domain = CookieAuthentication(
SECRET, LIFETIME, COOKIE_NAME, cookie_domain="camelot.bt"
)
cookie_authentication_secure = CookieAuthentication(
SECRET, LIFETIME, COOKIE_NAME, cookie_secure=False
)
cookie_authentication_httponly = CookieAuthentication(
SECRET, LIFETIME, COOKIE_NAME, cookie_httponly=False
)
@pytest.fixture @pytest.fixture
@ -29,24 +38,20 @@ def token():
@pytest.mark.authentication @pytest.mark.authentication
def test_default_name(cookie_authentication): def test_default_name():
assert cookie_authentication.name == "cookie" assert cookie_authentication.name == "cookie"
@pytest.mark.authentication @pytest.mark.authentication
class TestAuthenticate: class TestAuthenticate:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_missing_token( async def test_missing_token(self, mock_user_db, request_builder):
self, cookie_authentication, mock_user_db, request_builder
):
request = request_builder() request = request_builder()
authenticated_user = await cookie_authentication(request, mock_user_db) authenticated_user = await cookie_authentication(request, mock_user_db)
assert authenticated_user is None assert authenticated_user is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_invalid_token( async def test_invalid_token(self, mock_user_db, request_builder):
self, cookie_authentication, mock_user_db, request_builder
):
cookies = {} cookies = {}
cookies[COOKIE_NAME] = "foo" cookies[COOKIE_NAME] = "foo"
request = request_builder(cookies=cookies) request = request_builder(cookies=cookies)
@ -55,7 +60,7 @@ class TestAuthenticate:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_valid_token_missing_user_payload( async def test_valid_token_missing_user_payload(
self, cookie_authentication, mock_user_db, request_builder, token self, mock_user_db, request_builder, token
): ):
cookies = {} cookies = {}
cookies[COOKIE_NAME] = token() cookies[COOKIE_NAME] = token()
@ -64,9 +69,7 @@ class TestAuthenticate:
assert authenticated_user is None assert authenticated_user is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_valid_token( async def test_valid_token(self, mock_user_db, request_builder, token, user):
self, cookie_authentication, mock_user_db, request_builder, token, user
):
cookies = {} cookies = {}
cookies[COOKIE_NAME] = token(user) cookies[COOKIE_NAME] = token(user)
request = request_builder(cookies=cookies) request = request_builder(cookies=cookies)
@ -76,7 +79,19 @@ class TestAuthenticate:
@pytest.mark.authentication @pytest.mark.authentication
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_login_response(cookie_authentication, user): @pytest.mark.parametrize(
"cookie_authentication,path,domain,secure,httponly",
[
(cookie_authentication, "/", None, True, True),
(cookie_authentication_path, "/arthur", None, True, True),
(cookie_authentication_domain, "/", "camelot.bt", True, True),
(cookie_authentication_secure, "/", None, False, True),
(cookie_authentication_httponly, "/", None, True, False),
],
)
async def test_get_login_response(
user, cookie_authentication, path, domain, secure, httponly
):
response = Response() response = Response()
login_response = await cookie_authentication.get_login_response(user, response) login_response = await cookie_authentication.get_login_response(user, response)
@ -90,6 +105,22 @@ async def test_get_login_response(cookie_authentication, user):
cookie = cookies[0][1].decode("latin-1") cookie = cookies[0][1].decode("latin-1")
assert f"Max-Age={LIFETIME}" in cookie assert f"Max-Age={LIFETIME}" in cookie
assert f"Path={path}" in cookie
if domain:
assert f"Domain={domain}" in cookie
else:
assert "Domain=" not in cookie
if secure:
assert "Secure" in cookie
else:
assert "Secure" not in cookie
if httponly:
assert "HttpOnly" in cookie
else:
assert "HttpOnly" not in cookie
cookie_name_value = re.match(r"^(\w+)=([^;]+);", cookie) cookie_name_value = re.match(r"^(\w+)=([^;]+);", cookie)