mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-08-06 08:34:08 +00:00
fix: rename nonce by state_token
This commit is contained in:
+16
-25
@@ -174,15 +174,6 @@ async def profile(user: User = Security(bearer.require(role=Role.USER))):
|
|||||||
The `prefix` (for `BearerTokenAuth`), cookie name and `secret_key` (for
|
The `prefix` (for `BearerTokenAuth`), cookie name and `secret_key` (for
|
||||||
`CookieAuth`), and header name (for `APIKeyHeaderAuth`) are always preserved.
|
`CookieAuth`), and header name (for `APIKeyHeaderAuth`) are always preserved.
|
||||||
|
|
||||||
`.require()` instances work transparently inside `MultiAuth`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
multi = MultiAuth(
|
|
||||||
user_bearer.require(role=Role.USER),
|
|
||||||
org_bearer.require(role=Role.ADMIN),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## MultiAuth
|
## MultiAuth
|
||||||
|
|
||||||
[`MultiAuth`](../reference/security.md#fastapi_toolsets.security.MultiAuth) combines multiple auth sources into a single callable. Sources are tried in order; the first one that finds a credential wins.
|
[`MultiAuth`](../reference/security.md#fastapi_toolsets.security.MultiAuth) combines multiple auth sources into a single callable. Sources are tried in order; the first one that finds a credential wins.
|
||||||
@@ -285,24 +276,24 @@ Returns a `(authorization_url, token_url, userinfo_url)` tuple. `userinfo_url` i
|
|||||||
|
|
||||||
### Authorization redirect
|
### Authorization redirect
|
||||||
|
|
||||||
[`oauth_build_authorization_redirect()`](../reference/security.md#fastapi_toolsets.security.oauth_build_authorization_redirect) constructs the redirect to the provider's authorization page. It requires a `nonce` — a random CSRF token generated by [`oauth_generate_nonce()`](../reference/security.md#fastapi_toolsets.security.oauth_generate_nonce) — that must be stored server-side (e.g. in the session) and verified on the callback to prevent login-CSRF attacks (RFC 6749 §10.12):
|
[`oauth_build_authorization_redirect()`](../reference/security.md#fastapi_toolsets.security.oauth_build_authorization_redirect) constructs the redirect to the provider's authorization page. It requires a `state_token` — a random CSRF token generated by [`oauth_generate_state_token()`](../reference/security.md#fastapi_toolsets.security.oauth_generate_state_token) — that must be stored server-side (e.g. in the session) and verified on the callback to prevent login-CSRF attacks ([RFC 6749 §10.12](https://datatracker.ietf.org/doc/html/rfc6749#section-10.12)):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi_toolsets.security import oauth_build_authorization_redirect, oauth_generate_nonce
|
from fastapi_toolsets.security import oauth_build_authorization_redirect, oauth_generate_state_token
|
||||||
|
|
||||||
@app.get("/auth/google/login")
|
@app.get("/auth/google/login")
|
||||||
async def google_login(request: Request):
|
async def google_login(request: Request):
|
||||||
auth_url, _, _ = await oauth_resolve_provider_urls(GOOGLE_DISCOVERY_URL)
|
auth_url, _, _ = await oauth_resolve_provider_urls(GOOGLE_DISCOVERY_URL)
|
||||||
nonce = oauth_generate_nonce()
|
state_token = oauth_generate_state_token()
|
||||||
request.session["oauth_nonce"] = nonce # requires SessionMiddleware
|
request.session["oauth_state"] = state_token # requires SessionMiddleware
|
||||||
return oauth_build_authorization_redirect(
|
return oauth_build_authorization_redirect(
|
||||||
auth_url,
|
auth_url,
|
||||||
client_id=GOOGLE_CLIENT_ID,
|
client_id=GOOGLE_CLIENT_ID,
|
||||||
scopes="openid email profile",
|
scopes="openid email profile",
|
||||||
redirect_uri="https://myapp.com/auth/google/callback",
|
redirect_uri="https://myapp.com/auth/google/callback",
|
||||||
destination="/dashboard",
|
destination="/dashboard",
|
||||||
nonce=nonce,
|
state_token=state_token,
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -310,7 +301,7 @@ async def google_login(request: Request):
|
|||||||
|
|
||||||
[`oauth_fetch_userinfo()`](../reference/security.md#fastapi_toolsets.security.oauth_fetch_userinfo) performs the two-step exchange: it POSTs the authorization code to the token endpoint, then GETs the userinfo endpoint with the resulting access token.
|
[`oauth_fetch_userinfo()`](../reference/security.md#fastapi_toolsets.security.oauth_fetch_userinfo) performs the two-step exchange: it POSTs the authorization code to the token endpoint, then GETs the userinfo endpoint with the resulting access token.
|
||||||
|
|
||||||
On the callback, retrieve the stored nonce and pass it to [`oauth_decode_state()`](../reference/security.md#fastapi_toolsets.security.oauth_decode_state) to verify the CSRF token before processing the code:
|
On the callback, retrieve the stored token and pass it to [`oauth_decode_state()`](../reference/security.md#fastapi_toolsets.security.oauth_decode_state) to verify the CSRF token before processing the code:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from fastapi import HTTPException, Request
|
from fastapi import HTTPException, Request
|
||||||
@@ -318,11 +309,11 @@ from fastapi_toolsets.security import oauth_decode_state, oauth_fetch_userinfo
|
|||||||
|
|
||||||
@app.get("/auth/google/callback")
|
@app.get("/auth/google/callback")
|
||||||
async def google_callback(request: Request, code: str, state: str):
|
async def google_callback(request: Request, code: str, state: str):
|
||||||
# Pop nonce first — single-use, regardless of whether verification succeeds
|
# Pop token first — single-use, regardless of whether verification succeeds
|
||||||
nonce = request.session.pop("oauth_nonce", None)
|
state_token = request.session.pop("oauth_state", None)
|
||||||
if nonce is None:
|
if state_token is None:
|
||||||
raise HTTPException(status_code=400, detail="missing OAuth state")
|
raise HTTPException(status_code=400, detail="missing OAuth state")
|
||||||
destination = oauth_decode_state(state, expected_nonce=nonce, fallback="/")
|
destination = oauth_decode_state(state, expected_state_token=state_token, fallback="/")
|
||||||
if not destination.startswith("/"): # reject absolute URLs to prevent open-redirect
|
if not destination.startswith("/"): # reject absolute URLs to prevent open-redirect
|
||||||
destination = "/"
|
destination = "/"
|
||||||
|
|
||||||
@@ -346,16 +337,16 @@ Pass `required_scopes` to guard against providers silently granting fewer scopes
|
|||||||
|
|
||||||
### State encoding
|
### State encoding
|
||||||
|
|
||||||
[`oauth_encode_state()`](../reference/security.md#fastapi_toolsets.security.oauth_encode_state) and [`oauth_decode_state()`](../reference/security.md#fastapi_toolsets.security.oauth_decode_state) encode and decode the destination URL together with the CSRF nonce embedded in the OAuth `state` parameter. `oauth_decode_state` returns `fallback` if `state` is absent, malformed, or the nonce does not match:
|
[`oauth_encode_state()`](../reference/security.md#fastapi_toolsets.security.oauth_encode_state) and [`oauth_decode_state()`](../reference/security.md#fastapi_toolsets.security.oauth_decode_state) encode and decode the destination URL together with the CSRF token embedded in the OAuth `state` parameter. `oauth_decode_state` returns `fallback` if `state` is absent, malformed, or the token does not match:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from fastapi_toolsets.security import oauth_encode_state, oauth_decode_state
|
from fastapi_toolsets.security import oauth_encode_state, oauth_decode_state
|
||||||
|
|
||||||
nonce = "my-random-nonce"
|
state_token = oauth_generate_state_token()
|
||||||
encoded = oauth_encode_state("/dashboard", nonce)
|
encoded = oauth_encode_state("/dashboard", state_token)
|
||||||
decoded = oauth_decode_state(encoded, expected_nonce=nonce, fallback="/") # "/dashboard"
|
decoded = oauth_decode_state(encoded, expected_state_token=state_token, fallback="/") # "/dashboard"
|
||||||
decoded = oauth_decode_state(encoded, expected_nonce="wrong", fallback="/") # "/"
|
decoded = oauth_decode_state(encoded, expected_state_token="wrong", fallback="/") # "/"
|
||||||
decoded = oauth_decode_state(None, expected_nonce=nonce, fallback="/") # "/"
|
decoded = oauth_decode_state(None, expected_state_token=state_token, fallback="/") # "/"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from fastapi_toolsets.security import (
|
|||||||
oauth_decode_state,
|
oauth_decode_state,
|
||||||
oauth_encode_state,
|
oauth_encode_state,
|
||||||
oauth_fetch_userinfo,
|
oauth_fetch_userinfo,
|
||||||
oauth_generate_nonce,
|
oauth_generate_state_token,
|
||||||
oauth_resolve_provider_urls,
|
oauth_resolve_provider_urls,
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@@ -34,7 +34,7 @@ from fastapi_toolsets.security import (
|
|||||||
|
|
||||||
## ::: fastapi_toolsets.security.oauth_fetch_userinfo
|
## ::: fastapi_toolsets.security.oauth_fetch_userinfo
|
||||||
|
|
||||||
## ::: fastapi_toolsets.security.oauth_generate_nonce
|
## ::: fastapi_toolsets.security.oauth_generate_state_token
|
||||||
|
|
||||||
## ::: fastapi_toolsets.security.oauth_build_authorization_redirect
|
## ::: fastapi_toolsets.security.oauth_build_authorization_redirect
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from .oauth import (
|
|||||||
oauth_decode_state,
|
oauth_decode_state,
|
||||||
oauth_encode_state,
|
oauth_encode_state,
|
||||||
oauth_fetch_userinfo,
|
oauth_fetch_userinfo,
|
||||||
oauth_generate_nonce,
|
oauth_generate_state_token,
|
||||||
oauth_resolve_provider_urls,
|
oauth_resolve_provider_urls,
|
||||||
)
|
)
|
||||||
from .sources import APIKeyHeaderAuth, BearerTokenAuth, CookieAuth, MultiAuth
|
from .sources import APIKeyHeaderAuth, BearerTokenAuth, CookieAuth, MultiAuth
|
||||||
@@ -21,6 +21,6 @@ __all__ = [
|
|||||||
"oauth_decode_state",
|
"oauth_decode_state",
|
||||||
"oauth_encode_state",
|
"oauth_encode_state",
|
||||||
"oauth_fetch_userinfo",
|
"oauth_fetch_userinfo",
|
||||||
"oauth_generate_nonce",
|
"oauth_generate_state_token",
|
||||||
"oauth_resolve_provider_urls",
|
"oauth_resolve_provider_urls",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -103,13 +103,8 @@ async def oauth_fetch_userinfo(
|
|||||||
return userinfo_resp.json()
|
return userinfo_resp.json()
|
||||||
|
|
||||||
|
|
||||||
def oauth_generate_nonce() -> str:
|
def oauth_generate_state_token() -> str:
|
||||||
"""Generate a cryptographically random nonce for use as an OAuth CSRF token.
|
"""Generate a cryptographically random CSRF token for the OAuth ``state`` parameter."""
|
||||||
|
|
||||||
Call this before :func:`oauth_build_authorization_redirect`, persist the
|
|
||||||
returned value in the user's session or a ``Secure; HttpOnly; SameSite=Lax``
|
|
||||||
cookie, then verify it with :func:`oauth_decode_state` on the callback.
|
|
||||||
"""
|
|
||||||
return secrets.token_urlsafe(32)
|
return secrets.token_urlsafe(32)
|
||||||
|
|
||||||
|
|
||||||
@@ -120,7 +115,7 @@ def oauth_build_authorization_redirect(
|
|||||||
scopes: str,
|
scopes: str,
|
||||||
redirect_uri: str,
|
redirect_uri: str,
|
||||||
destination: str,
|
destination: str,
|
||||||
nonce: str,
|
state_token: str,
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
"""Return an OAuth 2.0 authorization ``RedirectResponse``.
|
"""Return an OAuth 2.0 authorization ``RedirectResponse``.
|
||||||
|
|
||||||
@@ -131,9 +126,9 @@ def oauth_build_authorization_redirect(
|
|||||||
redirect_uri: URI the provider should redirect back to after authorization.
|
redirect_uri: URI the provider should redirect back to after authorization.
|
||||||
destination: URL the user should be sent to after the full OAuth flow
|
destination: URL the user should be sent to after the full OAuth flow
|
||||||
completes (embedded in ``state``).
|
completes (embedded in ``state``).
|
||||||
nonce: CSRF token generated by :func:`oauth_generate_nonce`. Must be
|
state_token: CSRF token generated by :func:`oauth_generate_state_token`.
|
||||||
stored server-side (session or signed cookie) and verified via
|
Must be stored server-side (session or signed cookie) and verified via
|
||||||
:func:`oauth_decode_state` on the callback endpoint.
|
:func:`oauth_decode_state` on the callback endpoint (RFC 6749 §10.12).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A :class:`~fastapi.responses.RedirectResponse` to the provider's
|
A :class:`~fastapi.responses.RedirectResponse` to the provider's
|
||||||
@@ -145,32 +140,34 @@ def oauth_build_authorization_redirect(
|
|||||||
"response_type": "code",
|
"response_type": "code",
|
||||||
"scope": scopes,
|
"scope": scopes,
|
||||||
"redirect_uri": redirect_uri,
|
"redirect_uri": redirect_uri,
|
||||||
"state": oauth_encode_state(destination, nonce),
|
"state": oauth_encode_state(destination, state_token),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return RedirectResponse(f"{authorization_url}?{params}")
|
return RedirectResponse(f"{authorization_url}?{params}")
|
||||||
|
|
||||||
|
|
||||||
def oauth_encode_state(url: str, nonce: str) -> str:
|
def oauth_encode_state(url: str, state_token: str) -> str:
|
||||||
"""Encode a destination URL and CSRF nonce into an OAuth ``state`` parameter.
|
"""Encode a destination URL and CSRF token into an OAuth ``state`` parameter.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: Post-login destination URL.
|
url: Post-login destination URL.
|
||||||
nonce: CSRF token from :func:`oauth_generate_nonce`.
|
state_token: CSRF token from :func:`oauth_generate_state_token`.
|
||||||
"""
|
"""
|
||||||
payload = json.dumps({"n": nonce, "d": url}, separators=(",", ":"))
|
payload = json.dumps({"n": state_token, "d": url}, separators=(",", ":"))
|
||||||
return base64.urlsafe_b64encode(payload.encode()).decode()
|
return base64.urlsafe_b64encode(payload.encode()).decode()
|
||||||
|
|
||||||
|
|
||||||
def oauth_decode_state(state: str | None, *, expected_nonce: str, fallback: str) -> str:
|
def oauth_decode_state(
|
||||||
|
state: str | None, *, expected_state_token: str, fallback: str
|
||||||
|
) -> str:
|
||||||
"""Decode and CSRF-verify an OAuth ``state`` parameter.
|
"""Decode and CSRF-verify an OAuth ``state`` parameter.
|
||||||
|
|
||||||
Uses a constant-time comparison for the nonce to prevent timing attacks.
|
Uses a constant-time comparison for the CSRF token to prevent timing attacks.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
state: Raw ``state`` query parameter from the provider's callback.
|
state: Raw ``state`` query parameter from the provider's callback.
|
||||||
expected_nonce: The nonce stored before the authorization redirect.
|
expected_state_token: The token stored before the authorization redirect.
|
||||||
If the decoded nonce does not match, ``fallback`` is returned.
|
If it does not match the decoded value, ``fallback`` is returned.
|
||||||
fallback: URL to return when ``state`` is absent, malformed, or fails
|
fallback: URL to return when ``state`` is absent, malformed, or fails
|
||||||
CSRF verification.
|
CSRF verification.
|
||||||
|
|
||||||
@@ -178,7 +175,7 @@ def oauth_decode_state(state: str | None, *, expected_nonce: str, fallback: str)
|
|||||||
The destination URL embedded in ``state``, or ``fallback``.
|
The destination URL embedded in ``state``, or ``fallback``.
|
||||||
|
|
||||||
Important:
|
Important:
|
||||||
**Single-use**: delete the stored nonce from the session immediately
|
**Single-use**: delete the stored token from the session immediately
|
||||||
after calling this function — whether it matched or not — so that a
|
after calling this function — whether it matched or not — so that a
|
||||||
captured callback URL cannot be replayed.
|
captured callback URL cannot be replayed.
|
||||||
|
|
||||||
@@ -192,7 +189,7 @@ def oauth_decode_state(state: str | None, *, expected_nonce: str, fallback: str)
|
|||||||
padded = state + "=" * (-len(state) % 4)
|
padded = state + "=" * (-len(state) % 4)
|
||||||
payload = json.loads(base64.urlsafe_b64decode(padded).decode("utf-8"))
|
payload = json.loads(base64.urlsafe_b64decode(padded).decode("utf-8"))
|
||||||
if not isinstance(payload, dict) or not hmac.compare_digest(
|
if not isinstance(payload, dict) or not hmac.compare_digest(
|
||||||
payload.get("n", "").encode(), expected_nonce.encode()
|
payload.get("n", "").encode(), expected_state_token.encode()
|
||||||
):
|
):
|
||||||
return fallback
|
return fallback
|
||||||
return str(payload["d"])
|
return str(payload["d"])
|
||||||
|
|||||||
+31
-21
@@ -18,7 +18,7 @@ from fastapi_toolsets.security import (
|
|||||||
oauth_decode_state,
|
oauth_decode_state,
|
||||||
oauth_encode_state,
|
oauth_encode_state,
|
||||||
oauth_fetch_userinfo,
|
oauth_fetch_userinfo,
|
||||||
oauth_generate_nonce,
|
oauth_generate_state_token,
|
||||||
oauth_resolve_provider_urls,
|
oauth_resolve_provider_urls,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1013,56 +1013,64 @@ def _make_async_client_mock(get_return=None, post_return=None):
|
|||||||
|
|
||||||
class TestEncodeDecodeOAuthState:
|
class TestEncodeDecodeOAuthState:
|
||||||
def test_encode_returns_base64url_string(self):
|
def test_encode_returns_base64url_string(self):
|
||||||
result = oauth_encode_state("https://example.com/dashboard", "test-nonce")
|
result = oauth_encode_state("https://example.com/dashboard", "test-state-token")
|
||||||
assert isinstance(result, str)
|
assert isinstance(result, str)
|
||||||
assert "+" not in result
|
assert "+" not in result
|
||||||
assert "/" not in result
|
assert "/" not in result
|
||||||
|
|
||||||
def test_round_trip(self):
|
def test_round_trip(self):
|
||||||
url = "https://example.com/after-login?next=/home"
|
url = "https://example.com/after-login?next=/home"
|
||||||
nonce = "test-nonce"
|
state_token = "test-state-token"
|
||||||
assert (
|
assert (
|
||||||
oauth_decode_state(
|
oauth_decode_state(
|
||||||
oauth_encode_state(url, nonce), expected_nonce=nonce, fallback="/"
|
oauth_encode_state(url, state_token),
|
||||||
|
expected_state_token=state_token,
|
||||||
|
fallback="/",
|
||||||
)
|
)
|
||||||
== url
|
== url
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_decode_none_returns_fallback(self):
|
def test_decode_none_returns_fallback(self):
|
||||||
assert (
|
assert (
|
||||||
oauth_decode_state(None, expected_nonce="any", fallback="/home") == "/home"
|
oauth_decode_state(None, expected_state_token="any", fallback="/home")
|
||||||
|
== "/home"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_decode_null_string_returns_fallback(self):
|
def test_decode_null_string_returns_fallback(self):
|
||||||
assert (
|
assert (
|
||||||
oauth_decode_state("null", expected_nonce="any", fallback="/home")
|
oauth_decode_state("null", expected_state_token="any", fallback="/home")
|
||||||
== "/home"
|
== "/home"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_decode_invalid_base64_returns_fallback(self):
|
def test_decode_invalid_base64_returns_fallback(self):
|
||||||
assert (
|
assert (
|
||||||
oauth_decode_state(
|
oauth_decode_state(
|
||||||
"!!!notbase64!!!", expected_nonce="any", fallback="/home"
|
"!!!notbase64!!!", expected_state_token="any", fallback="/home"
|
||||||
)
|
)
|
||||||
== "/home"
|
== "/home"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_decode_handles_missing_padding(self):
|
def test_decode_handles_missing_padding(self):
|
||||||
url = "https://example.com/x"
|
url = "https://example.com/x"
|
||||||
nonce = "test-nonce"
|
state_token = "test-state-token"
|
||||||
encoded = oauth_encode_state(url, nonce).rstrip("=")
|
encoded = oauth_encode_state(url, state_token).rstrip("=")
|
||||||
assert oauth_decode_state(encoded, expected_nonce=nonce, fallback="/") == url
|
|
||||||
|
|
||||||
def test_decode_wrong_nonce_returns_fallback(self):
|
|
||||||
url = "https://example.com/dashboard"
|
|
||||||
encoded = oauth_encode_state(url, "correct-nonce")
|
|
||||||
assert (
|
assert (
|
||||||
oauth_decode_state(encoded, expected_nonce="wrong-nonce", fallback="/")
|
oauth_decode_state(encoded, expected_state_token=state_token, fallback="/")
|
||||||
|
== url
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_decode_wrong_state_token_returns_fallback(self):
|
||||||
|
url = "https://example.com/dashboard"
|
||||||
|
encoded = oauth_encode_state(url, "correct-token")
|
||||||
|
assert (
|
||||||
|
oauth_decode_state(
|
||||||
|
encoded, expected_state_token="wrong-token", fallback="/"
|
||||||
|
)
|
||||||
== "/"
|
== "/"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_generate_nonce_is_random(self):
|
def test_generate_state_token_is_random(self):
|
||||||
assert oauth_generate_nonce() != oauth_generate_nonce()
|
assert oauth_generate_state_token() != oauth_generate_state_token()
|
||||||
|
|
||||||
|
|
||||||
class TestBuildAuthorizationRedirect:
|
class TestBuildAuthorizationRedirect:
|
||||||
@@ -1075,19 +1083,19 @@ class TestBuildAuthorizationRedirect:
|
|||||||
scopes="openid email",
|
scopes="openid email",
|
||||||
redirect_uri="https://app.example.com/callback",
|
redirect_uri="https://app.example.com/callback",
|
||||||
destination="https://app.example.com/dashboard",
|
destination="https://app.example.com/dashboard",
|
||||||
nonce="test-nonce",
|
state_token="test-state-token",
|
||||||
)
|
)
|
||||||
assert isinstance(response, RedirectResponse)
|
assert isinstance(response, RedirectResponse)
|
||||||
|
|
||||||
def test_redirect_location_contains_all_params(self):
|
def test_redirect_location_contains_all_params(self):
|
||||||
nonce = "test-nonce"
|
state_token = "test-state-token"
|
||||||
response = oauth_build_authorization_redirect(
|
response = oauth_build_authorization_redirect(
|
||||||
"https://auth.example.com/authorize",
|
"https://auth.example.com/authorize",
|
||||||
client_id="my-client",
|
client_id="my-client",
|
||||||
scopes="openid email",
|
scopes="openid email",
|
||||||
redirect_uri="https://app.example.com/callback",
|
redirect_uri="https://app.example.com/callback",
|
||||||
destination="https://app.example.com/dashboard",
|
destination="https://app.example.com/dashboard",
|
||||||
nonce=nonce,
|
state_token=state_token,
|
||||||
)
|
)
|
||||||
location = response.headers["location"]
|
location = response.headers["location"]
|
||||||
parsed = urlparse(location)
|
parsed = urlparse(location)
|
||||||
@@ -1101,7 +1109,9 @@ class TestBuildAuthorizationRedirect:
|
|||||||
assert params["scope"] == ["openid email"]
|
assert params["scope"] == ["openid email"]
|
||||||
assert params["redirect_uri"] == ["https://app.example.com/callback"]
|
assert params["redirect_uri"] == ["https://app.example.com/callback"]
|
||||||
assert (
|
assert (
|
||||||
oauth_decode_state(params["state"][0], expected_nonce=nonce, fallback="")
|
oauth_decode_state(
|
||||||
|
params["state"][0], expected_state_token=state_token, fallback=""
|
||||||
|
)
|
||||||
== "https://app.example.com/dashboard"
|
== "https://app.example.com/dashboard"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user