diff --git a/docs/config.md b/docs/config.md index 91a0b8a..0061abb 100644 --- a/docs/config.md +++ b/docs/config.md @@ -4,19 +4,21 @@ The following key/value pairs are used for configurating the extension: -| **Config Name** | **Description** | -|---------------------------------------|------------------------------------------------------------------------------------------| -| `AWS_COGNITO_DISABLED` | Globally disable auth with Cognito (default=False) | -| `AWS_REGION` | Region the user pool was created | -| `AWS_COGNITO_DOMAIN` | The domain name of the user pool | -| `AWS_COGNITO_USER_POOL_ID` | The ID of the user pool | -| `AWS_COGNITO_USER_POOL_CLIENT_ID` | The user pool app client ID (*) | -| `AWS_COGNITO_USER_POOL_CLIENT_SECRET` | The user pool app client secret (*) [Optional for public Cognito clients] | -| `AWS_COGNITO_REDIRECT_URL` | The full URL of the route that handles post-login flow | -| `AWS_COGNITO_LOGOUT_URL` | The full URL of the route that handles post-logout flow | -| `AWS_COGNITO_COOKIE_AGE_SECONDS` | (Optional) How long to store the access token cookie. (default=1800) | -| `AWS_COGNITO_EXPIRATION_LEEWAY` | (Optional) Leeway (in seconds) when checking for token expiry (default=0) | -| `AWS_COGNITO_SCOPES` | (Optional) List of scopes to request from Cognito, if None (default) will get all scopes | +| **Config Name** | **Description** | +|---------------------------------------|-------------------------------------------------------------------------------------------| +| `AWS_COGNITO_DISABLED` | Globally disable auth with Cognito (default=False) | +| `AWS_REGION` | Region the user pool was created | +| `AWS_COGNITO_DOMAIN` | The domain name of the user pool | +| `AWS_COGNITO_USER_POOL_ID` | The ID of the user pool | +| `AWS_COGNITO_USER_POOL_CLIENT_ID` | The user pool app client ID (*) | +| `AWS_COGNITO_USER_POOL_CLIENT_SECRET` | The user pool app client secret (*) [Optional for public Cognito clients] | +| `AWS_COGNITO_REDIRECT_URL` | The full URL of the route that handles post-login flow | +| `AWS_COGNITO_LOGOUT_URL` | The full URL of the route that handles post-logout flow | +| `AWS_COGNITO_COOKIE_AGE_SECONDS` | (Optional) How long to store the access token cookie. (default=1800) | +| `AWS_COGNITO_EXPIRATION_LEEWAY` | (Optional) Leeway (in seconds) when checking for token expiry (default=0) | +| `AWS_COGNITO_SCOPES` | (Optional) List of scopes to request from Cognito, if None (default) will get all scopes | +| `AWS_COGNITO_COOKIE_DOMAIN` | (Optional) Domain used for setting a cookie (default=None) | +| `AWS_COGNITO_COOKIE_SAMESITE` | (Optional) Setting for "samesite" on the cookie. Choose "lax", "strict" or None (default) | (*) To obtain these values, navigate to the user pool in the AWS Cognito console, then head to the "App Integration" tab. Under the app client list, select the app client and you should be able to view the Client ID and Client Secret diff --git a/src/flask_cognito_lib/config.py b/src/flask_cognito_lib/config.py index f4ba74e..0571451 100644 --- a/src/flask_cognito_lib/config.py +++ b/src/flask_cognito_lib/config.py @@ -109,7 +109,32 @@ def cognito_scopes(self) -> Optional[List[str]]: Return the scopes to request from Cognito. If None, all supported scopes are returned """ - return get("AWS_COGNITO_SCOPES", required=False, default=None) + return get("AWS_COGNITO_SCOPES", required=False) + + @property + def cookie_domain(self) -> str: + """Return the domain used for the cookie. + + Used if you want to set a cross-domain cookie. + For example, domain=".example.com" will set a cookie that is readable + by the domain www.example.com, foo.example.com etc. + + If not set (default) then the cookie will only be readable by the + domain that set it. + """ + return get("AWS_COGNITO_COOKIE_DOMAIN", required=False) + + @property + def cookie_samesite(self) -> str: + """Return the property to set for "samesite" on the cookie + + The SameSite attribute lets servers specify whether/when cookies are + sent with cross-site requests (where Site is defined by the registrable + domain and the scheme: http or https). This provides some protection + against cross-site request forgery attacks (CSRF). + It takes three possible values: Strict, Lax, and None. + """ + return get("AWS_COGNITO_COOKIE_SAMESITE", required=False) @property def issuer(self) -> str: diff --git a/src/flask_cognito_lib/decorators.py b/src/flask_cognito_lib/decorators.py index bb05c6e..90b920b 100644 --- a/src/flask_cognito_lib/decorators.py +++ b/src/flask_cognito_lib/decorators.py @@ -125,6 +125,8 @@ def wrapper(*args, **kwargs): max_age=cfg.max_cookie_age_seconds, httponly=True, secure=True, + samesite=cfg.cookie_samesite, + domain=cfg.cookie_domain, ) return resp @@ -140,7 +142,7 @@ def wrapper(*args, **kwargs): with app.app_context(): # logout at cognito and remove the cookies resp = redirect(cfg.logout_endpoint) - resp.delete_cookie(key=cfg.COOKIE_NAME) + resp.delete_cookie(key=cfg.COOKIE_NAME, domain=cfg.cookie_domain) # Cognito will redirect to the sign-out URL (if set) or else use # the callback URL diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 7ce7e8e..1db877b 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -95,6 +95,46 @@ def test_cognito_login_callback(client, cfg, access_token, token_response): assert "user_info" in session +def test_cognito_login_cookie_domain(client, cfg, access_token, token_response): + # set a domain for the cookie + client.application.config["AWS_COGNITO_COOKIE_DOMAIN"] = ".example.com" + + with client as c: + with c.session_transaction() as sess: + sess["code_verifier"] = "1234" + sess["state"] = "5678" + sess["nonce"] = "MSln6nvPIIBVMhsNUOtUCtssceUKz4dhCRZi5QZRU4A=" + + # returns OK and sets the cookie + response = client.get("/postlogin") + assert response.status_code == 200 + assert response.data.decode("utf-8") == "ok" + + # check that the cookie is being set with the correct domain configuration + assert "Domain=example.com" in response.headers["Set-Cookie"] + + +def test_cognito_login_cookie_samesite(client, cfg, access_token, token_response): + # set a domain for the cookie + client.application.config["AWS_COGNITO_COOKIE_DOMAIN"] = ".example.com" + client.application.config["AWS_COGNITO_COOKIE_SAMESITE"] = "Strict" + + with client as c: + with c.session_transaction() as sess: + sess["code_verifier"] = "1234" + sess["state"] = "5678" + sess["nonce"] = "MSln6nvPIIBVMhsNUOtUCtssceUKz4dhCRZi5QZRU4A=" + + # returns OK and sets the cookie + response = client.get("/postlogin") + assert response.status_code == 200 + assert response.data.decode("utf-8") == "ok" + + # check that the cookie is being set with the correct domain configuration + assert "Domain=example.com" in response.headers["Set-Cookie"] + assert "SameSite=Strict" in response.headers["Set-Cookie"] + + def test_cognito_logout(client, cfg): # should 302 redirect to cognito response = client.get("/logout")