-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rework handling of missing state during login
If the authentication state is missing entirely, not incorrect, during return from authentication, the most likely explanation is that the user attempted multiple simultaneous logins and then finished the authentication in another browser window, thus invalidating the state. In this case, redirect the user to their destination without further processing. If they did authentication separately, this is the (mostly) correct thing to do; they will go to the authenticated page, although their session might not be as currently as they would like. If they're not authenticated, this will restart the authentication process with new state, which should be the correct thing to do. There is some risk of a redirect loop, but hopefully we won't go through that loop more than once, so browsers should cope.
- Loading branch information
Showing
3 changed files
with
66 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
### Bug fixes | ||
|
||
- If the user returns from authentication and no longer has login state in their cookie, redirect them to the destination URL without further processing instead of returning an authentication state mismatch error. The most likely cause of this state is that the user authenticated from another browser tab while this authentication is pending, so Gafaelfawr should use their existing token or restart the authentication process. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,8 @@ | |
|
||
from __future__ import annotations | ||
|
||
import base64 | ||
import os | ||
from unittest.mock import ANY | ||
from urllib.parse import parse_qs, urlparse | ||
|
||
|
@@ -11,11 +13,14 @@ | |
from safir.testing.slack import MockSlackWebhook | ||
|
||
from gafaelfawr.config import Config | ||
from gafaelfawr.constants import COOKIE_NAME | ||
from gafaelfawr.dependencies.config import config_dependency | ||
from gafaelfawr.factory import Factory | ||
from gafaelfawr.models.github import GitHubTeam, GitHubUserInfo | ||
from gafaelfawr.models.state import State | ||
from gafaelfawr.providers.github import GitHubProvider | ||
|
||
from ..support.constants import TEST_HOSTNAME | ||
from ..support.github import mock_github | ||
from ..support.logging import parse_log | ||
|
||
|
@@ -546,3 +551,48 @@ async def test_unicode_name( | |
{"name": "org-a-team", "id": 1000}, | ||
], | ||
} | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_invalid_state( | ||
client: AsyncClient, config: Config, respx_mock: respx.Router | ||
) -> None: | ||
user_info = GitHubUserInfo( | ||
name="GitHub User", | ||
username="githubuser", | ||
uid=123456, | ||
email="[email protected]", | ||
teams=[], | ||
) | ||
return_url = "https://example.com/foo" | ||
|
||
mock_github(respx_mock, "some-code", user_info) | ||
r = await client.get("/login", params={"rd": return_url}) | ||
assert r.status_code == 307 | ||
url = urlparse(r.headers["Location"]) | ||
query = parse_qs(url.query) | ||
|
||
# Change the state to something that won't match. | ||
state = await State.from_cookie(r.cookies[COOKIE_NAME]) | ||
state.state = base64.urlsafe_b64encode(os.urandom(16)).decode() | ||
client.cookies.set(COOKIE_NAME, state.to_cookie(), domain=TEST_HOSTNAME) | ||
|
||
# We should now get an error from the login endpoint. | ||
r = await client.get( | ||
"/login", params={"code": "some-code", "state": query["state"][0]} | ||
) | ||
assert r.status_code == 403 | ||
assert "Authentication state mismatch" in r.text | ||
|
||
# Change the state to None. | ||
state.state = None | ||
client.cookies.set(COOKIE_NAME, state.to_cookie(), domain=TEST_HOSTNAME) | ||
|
||
# Now we should get a simple redirect to the return URL even though the | ||
# authentication isn't complete, since the code should assume, given the | ||
# empty state, that the user may have logged in via another window. | ||
r = await client.get( | ||
"/login", params={"code": "some-code", "state": query["state"][0]} | ||
) | ||
assert r.status_code == 307 | ||
assert r.headers["Location"] == return_url |