Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ application is running at `myapp.org`, the authentication flow works as follows:
- The user's browser is then redirected back to your application, towards the
`myapp.org/callback` endpoint, along with yet another code.
- Your server then handles this request by exchanging this code with the
`soauth.org/code` endpoint. The response to this request is two tokens:
an `access_token`, and a `refresh_token`. It also includes the `redirect`
url which was pulled from your user's browser when they initially were directed
`soauth.org/code` endpoint. The response to this request is two sets of tokens:
The **security cookies** (`access_token` and `refresh_token`) are set as HttpOnly and
**state info cookies** (`valid_access_token` and `valid_refresh_token`) that your frontend JavaScript can read. It also includes the `redirect` url which was pulled from your user's browser when they initially were directed
to the login page.
- The state cookies contain no sensitive information—just simple string value "True" to tell your frontend application about the current authentication status.
- Your server sets the `access_token` and `refresh_token` as cookies, which
are included in every request to the server.

Expand Down Expand Up @@ -96,8 +97,27 @@ In practice:
When this process takes place, the old refresh token is expired on the server-side, meaning
it can never be used again. This helps protect against stolen credential attacks.

The state cookies are carefully synchronized with their secure counterparts:
- `valid_access_token` expires at the same time as the `access_token`.
- `valid_refresh_token` expires at the same time as the `refresh_token`.
- Both are automatically updated during refresh operations.
- Both are removed when the user logs out.

Frontend State Awareness
------------------------

While the server side handles all authentication decisions using the secure HttpOnly cookies, The **JavaScript-readable state cookies** help your frontend application understand the current authentication state without making API calls.

These state cookies work alongside the secure authentication system:

**JavaScript-readable cookies:**
- `valid_access_token`: If exists indicates whether the current access token is valid (value set to "True")
- `valid_refresh_token`: If exists indicates whether the user can obtain a new access token (value set to "True")


**Next**: [setup an app](create.md)

[^1]: Note the use here of `referrerpolicy="no-referrer-when-downgrade"`; if this is not used
the full path of your client is not sent to the authentication server, and the eventual
redirect won't work.
redirect won't work.

3 changes: 1 addition & 2 deletions docs/sample.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ add exception handlers (which we won't cover until the end here, see
[the bottom section](create.md) in the app creation documentation) for
things like 401/403 authorization errors.

Now, let's add a way for users to login (and get access tokens and refresh
keys):
Now, let's add a way for users to login (and get security and state tokens for both access and refresh):
```python
from fastapi import Request
from fastapi.responses import HTMLResponse
Expand Down
35 changes: 34 additions & 1 deletion soauth/toolkit/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ async def authenticate(self, conn: Request):
access_token_name=self.access_token_name,
refresh_token_name=self.refresh_token_name,
)


# Two possibilities: either we have the access token as a cookie, or we
# have it as a 'Bearer' token in the headers.
Expand Down Expand Up @@ -317,6 +318,21 @@ def key_expired_handler(request: Request, exc: KeyExpiredError) -> RedirectRespo
expires=content.refresh_token_expires,
)

response.set_cookie(
key="valid_refresh_token",
value="True",
expires=content.refresh_token_expires,
httponly=False,
)

response.set_cookie(
key="validate_access_token",
value="True",
expires=content.access_token_expires,
httponly=False,
)


log.info("tk.starlette.expired.refreshed")

return response
Expand Down Expand Up @@ -347,7 +363,8 @@ def key_decode_handler(request: Request, exc: KeyDecodeError) -> RedirectRespons

response.delete_cookie(access_token_name)
response.delete_cookie(refresh_token_name)

response.delete_cookie("valid_refresh_token")
response.delete_cookie("validate_access_token")
log.info("tk.starlette.decode.redirecting")

return response
Expand Down Expand Up @@ -429,6 +446,20 @@ async def handle_redirect(code: str, state: str, request: Request) -> RedirectRe
httponly=True,
)

response.set_cookie(
key="valid_refresh_token",
value="True",
expires=content.refresh_token_expires,
httponly=False,
)

response.set_cookie(
key="validate_access_token",
value="True",
expires=content.access_token_expires,
httponly=False,
)

return response


Expand Down Expand Up @@ -456,5 +487,7 @@ async def logout(request: Request) -> RedirectResponse:

response.delete_cookie(refresh_token_name)
response.delete_cookie(access_token_name)
response.delete_cookie("valid_refresh_token")
response.delete_cookie("validate_access_token")

return response
Loading