Properly handling and validating the state for login.gov

Signed-off-by: Cliff Hill <clifford.hill@gsa.gov>
This commit is contained in:
Cliff Hill
2024-10-25 16:28:25 -04:00
parent a02448a0fa
commit a39c844c30
3 changed files with 37 additions and 16 deletions

View File

@@ -35,24 +35,24 @@ def index():
if current_user and current_user.is_authenticated:
return redirect(url_for("main.choose_account"))
token = generate_token(
# make and store the state
state = generate_token(
str(request.remote_addr),
current_app.config["SECRET_KEY"],
current_app.config["DANGEROUS_SALT"],
)
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
# handle unit tests
current_app.logger.warning(f"############### {str(request.remote_addr)}")
state_key = f"login-nonce-{unquote(state)}"
redis_client.set(state_key, state)
# make and store the nonce
nonce = secrets.token_urlsafe()
nonce_key = f"login-nonce-{unquote(nonce)}"
redis_client.set(nonce_key, nonce)
redis_key = f"login-nonce-{unquote(nonce)}"
redis_client.set(redis_key, nonce)
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
if url is not None:
url = url.replace("NONCE", nonce)
url = url.replace("STATE", token)
url = url.replace("STATE", state)
return render_template(
"views/signedout.html",
sms_rate=CURRENT_SMS_RATE,

View File

@@ -66,8 +66,8 @@ def _get_access_token(code): # pragma: no cover
response_json = response.json()
id_token = get_id_token(response_json)
nonce = id_token["nonce"]
redis_key = f"login-nonce-{unquote(nonce)}"
stored_nonce = redis_client.get(redis_key).decode("utf8")
nonce_key = f"login-nonce-{unquote(nonce)}"
stored_nonce = redis_client.get(nonce_key).decode("utf8")
if nonce != stored_nonce:
current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}")
@@ -99,6 +99,12 @@ def _do_login_dot_gov(): # $ pragma: no cover
# start login.gov
code = request.args.get("code")
state = request.args.get("state")
state_key = f"login-state-{unquote(state)}"
stored_state = redis_client.get(state_key).decode("utf8")
if state != stored_state:
current_app.logger.error(f"State Error: {state} != {stored_state}")
abort(403)
login_gov_error = request.args.get("error")
if login_gov_error:
@@ -203,21 +209,23 @@ def sign_in(): # pragma: no cover
return redirect(redirect_url)
return redirect(url_for("main.show_accounts_or_dashboard"))
token = generate_token(
state = generate_token(
str(request.remote_addr),
current_app.config["SECRET_KEY"],
current_app.config["DANGEROUS_SALT"],
)
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
state_key = f"login-state-{unquote(state)}"
redis_client.set(state_key, state)
nonce = secrets.token_urlsafe()
redis_key = f"login-nonce-{unquote(nonce)}"
redis_client.set(redis_key, nonce)
nonce_key = f"login-nonce-{unquote(nonce)}"
redis_client.set(nonce_key, nonce)
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
# handle unit tests
if url is not None:
url = url.replace("NONCE", nonce)
url = url.replace("STATE", token)
url = url.replace("STATE", state)
return render_template(
"views/signin.html",

View File

@@ -1,12 +1,15 @@
import secrets
from urllib.parse import unquote
from flask import current_app, request
from app import redis_client
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
from app.utils.user_permissions import (
all_ui_permissions,
translate_permissions_from_ui_to_db,
)
from notifications_utils.url_safe_token import generate_token
class InviteApiClient(NotifyAdminAPIClient):
@@ -37,11 +40,21 @@ class InviteApiClient(NotifyAdminAPIClient):
}
data = _attach_current_user(data)
# make and store the state
state = generate_token(
str(request.remote_addr),
current_app.config["SECRET_KEY"],
current_app.config["DANGEROUS_SALT"],
)
state_key = f"login-state-{unquote(state)}"
redis_client.set(state_key, state)
# make and store the nonce
nonce = secrets.token_urlsafe()
redis_key = f"login-nonce-{unquote(nonce)}"
redis_client.set(f"{redis_key}", nonce) # save the nonce to redis.
data["nonce"] = nonce # This is passed to api for the invite url.
data["state"] = state # This is passed to api for the invite url.
resp = self.post(url=f"/service/{service_id}/invite", data=data)
return resp["data"]