Make registration start idempotent
This commit is contained in:
parent
e0399cf235
commit
d6873b84ae
4 changed files with 99 additions and 7 deletions
|
|
@ -579,6 +579,8 @@ class RegistrationSession:
|
|||
started_by_subject: str | None = None
|
||||
applicant_username: str | None = None
|
||||
client_id: str | None = None
|
||||
start_idempotency_hash: str | None = None
|
||||
start_request_hash: str | None = None
|
||||
provisioning_resume_hash: str | None = None
|
||||
correlation_id: str | None = None
|
||||
created_at: datetime = field(default_factory=utc_now)
|
||||
|
|
|
|||
|
|
@ -198,8 +198,10 @@ class PortalApplication:
|
|||
if not self.public_registration or self.registration_verification is None:
|
||||
raise NotFoundError("public registration is unavailable")
|
||||
token = secrets.token_urlsafe(32)
|
||||
idempotency_key = secrets.token_urlsafe(24)
|
||||
return self._html(
|
||||
start_response, self._registration_form(token), correlation_id,
|
||||
start_response,
|
||||
self._registration_form(token, idempotency_key), correlation_id,
|
||||
extra_headers=[("Set-Cookie", self._registration_csrf_cookie(token))],
|
||||
)
|
||||
if path == "/register" and method == "POST":
|
||||
|
|
@ -919,6 +921,34 @@ class PortalApplication:
|
|||
if tenant not in self.registration_tenants:
|
||||
raise ValidationError("tenant is not eligible for registration")
|
||||
|
||||
raw_idempotency_key = str(
|
||||
body.get("idempotency_key") if browser
|
||||
else environ.get("HTTP_IDEMPOTENCY_KEY", "")
|
||||
)
|
||||
if len(raw_idempotency_key) < 16 or len(raw_idempotency_key) > 256:
|
||||
raise ValidationError("registration idempotency key is invalid")
|
||||
idempotency_hash = hmac.new(
|
||||
self.trusted_proxy_secret.encode(), raw_idempotency_key.encode(),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
request_hash = hashlib.sha256(json.dumps({
|
||||
"username": username, "email": email, "display_name": display_name,
|
||||
"client_id": client_id, "tenant": tenant,
|
||||
}, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
||||
previous = next((
|
||||
item for item in self.service.store.all_registration_sessions()
|
||||
if item.start_idempotency_hash
|
||||
and hmac.compare_digest(item.start_idempotency_hash, idempotency_hash)
|
||||
), None)
|
||||
if previous is not None:
|
||||
if not previous.start_request_hash or not hmac.compare_digest(
|
||||
previous.start_request_hash, request_hash
|
||||
):
|
||||
raise ConflictError("registration idempotency key was reused")
|
||||
return self._registration_requested_response(
|
||||
start_response, correlation_id, browser
|
||||
)
|
||||
|
||||
applicant_subject = f"applicant_{secrets.token_hex(16)}"
|
||||
actor = Actor(
|
||||
issuer="urn:netkingdom:public-registration",
|
||||
|
|
@ -937,6 +967,11 @@ class PortalApplication:
|
|||
applicant_username=username,
|
||||
client_id=client_id,
|
||||
)
|
||||
session = replace(
|
||||
session, start_idempotency_hash=idempotency_hash,
|
||||
start_request_hash=request_hash,
|
||||
)
|
||||
self.service.store.save_registration_session(session)
|
||||
self.registration_verification.request(
|
||||
RegistrationVerificationRequest(
|
||||
registration_id=session.registration_id,
|
||||
|
|
@ -948,6 +983,13 @@ class PortalApplication:
|
|||
display_name=display_name,
|
||||
)
|
||||
)
|
||||
return self._registration_requested_response(
|
||||
start_response, correlation_id, browser
|
||||
)
|
||||
|
||||
def _registration_requested_response(
|
||||
self, start_response: StartResponse, correlation_id: str, browser: bool
|
||||
) -> Iterable[bytes]:
|
||||
if browser:
|
||||
return self._html(
|
||||
start_response,
|
||||
|
|
@ -960,10 +1002,8 @@ class PortalApplication:
|
|||
correlation_id,
|
||||
)
|
||||
return self._json(
|
||||
start_response,
|
||||
"202 Accepted",
|
||||
{"status": "verification_requested"},
|
||||
correlation_id,
|
||||
start_response, "202 Accepted",
|
||||
{"status": "verification_requested"}, correlation_id,
|
||||
)
|
||||
|
||||
def _verify_public_registration(
|
||||
|
|
@ -1325,7 +1365,7 @@ class PortalApplication:
|
|||
+ identity,
|
||||
)
|
||||
|
||||
def _registration_form(self, csrf_token: str) -> str:
|
||||
def _registration_form(self, csrf_token: str, idempotency_key: str) -> str:
|
||||
client_options = "".join(
|
||||
f'<option value="{escape(item)}">{escape(item)}</option>'
|
||||
for item in sorted(self.registration_clients)
|
||||
|
|
@ -1340,6 +1380,7 @@ class PortalApplication:
|
|||
<p>We will verify your email before creating an identity.</p>
|
||||
<form method="post" action="/register">
|
||||
<input type="hidden" name="csrf_token" value="{escape(csrf_token)}">
|
||||
<input type="hidden" name="idempotency_key" value="{escape(idempotency_key)}">
|
||||
<label>Username <input name="username" required minlength="3" maxlength="32" pattern="[A-Za-z][A-Za-z0-9._-]+" autocomplete="username"></label>
|
||||
<label>Email <input name="email" type="email" required autocomplete="email"></label>
|
||||
<label>Display name <input name="display_name" maxlength="200" autocomplete="name"></label>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue