diff --git a/app.py b/app.py index 1025562..85488f6 100644 --- a/app.py +++ b/app.py @@ -128,47 +128,44 @@ def demo(demo_name): return render_template(template, **params) ################################# -# Risk / Filter (registration) +# Filter (registration) ################################# @app.route('/evaluate_signup', methods=['POST']) def evaluate_signup(): - name = request.json.get("name") email = request.json["email"] request_token = request.json["request_token"] castle_type = "$registration" - # An email that's already taken (the known demo user) is a failed - # registration and goes to /filter; a fresh sign-up is risk-assessed. + # A registration is evaluated before the account exists, so it is anonymous + # activity sent to /filter with the form params (email/phone only). A brand- + # new email is an attempt; an email that already belongs to a user is a + # failed registration, resolved to that user via matching_user_id. if email == os.getenv("valid_username"): castle_status = "$failed" - castle_api_endpoint = "filter" + payload_to_castle = { + 'type': castle_type, + 'status': castle_status, + 'params': {'email': email}, + 'matching_user_id': os.getenv("valid_user_id"), + 'request_token': request_token, + } else: - castle_status = "$succeeded" - castle_api_endpoint = "risk" - - payload_to_castle = { - 'type': castle_type, - 'status': castle_status, - 'user': { - 'id': os.getenv("valid_user_id"), - 'email': email, - 'name': name, - }, - 'request_token': request_token, - } + castle_status = "$attempted" + payload_to_castle = { + 'type': castle_type, + 'status': castle_status, + 'params': {'email': email}, + 'request_token': request_token, + } castle = Client.from_request(request) - - if castle_api_endpoint == "risk": - verdict = castle.risk(payload_to_castle) - else: - verdict = castle.filter(payload_to_castle) + verdict = castle.filter(payload_to_castle) return { - "api_endpoint": castle_api_endpoint, + "api_endpoint": "filter", "payload_to_castle": payload_to_castle, "result": verdict, "castle_type": castle_type, @@ -176,73 +173,62 @@ def evaluate_signup(): }, 200, {'ContentType': 'application/json'} ################################# -# Risk / Filter (login) +# Filter -> Risk (login) ################################# @app.route('/evaluate_login', methods=['POST']) def evaluate_login(): - global registered_at - - print(request.json) - email = request.json["email"] password = request.json["password"] request_token = request.json["request_token"] - # check validity of username + password combo - if email == os.getenv("valid_username"): + castle_type = "$login" - user_id = os.getenv("valid_user_id") + # A login reuses one request token across two calls: first Filter the attempt + # while the visitor is still anonymous, then — on success — assess the + # authenticated user with Risk. A failed attempt stays on Filter. + castle = Client.from_request(request) - if password == os.getenv("valid_password"): - castle_type = "$login" - castle_status = "$succeeded" - castle_api_endpoint = "risk" + def run_step(api_endpoint, castle_status, fields): + payload_to_castle = { + 'type': castle_type, + 'status': castle_status, + **fields, + 'request_token': request_token, + } + if api_endpoint == "risk": + verdict = castle.risk(payload_to_castle) else: - castle_type = "$login" - castle_status = "$failed" - castle_api_endpoint = "filter" + verdict = castle.filter(payload_to_castle) + return { + "api_endpoint": api_endpoint, + "payload_to_castle": payload_to_castle, + "result": verdict, + "castle_type": castle_type, + "castle_status": castle_status, + } + + # Step 1 — always filter the attempt up front (anonymous -> params). + steps = [run_step("filter", "$attempted", {'params': {'email': email}})] + + # Step 2 — the outcome, on the same request token. + if email == os.getenv("valid_username") and password == os.getenv("valid_password"): + steps.append(run_step("risk", "$succeeded", { + 'user': { + 'id': os.getenv("valid_user_id"), + 'email': email, + 'registered_at': registered_at, + }, + })) else: - castle_api_endpoint = "filter" - castle_type = "$login" - castle_status = "$failed" - user_id = None - registered_at = None + fields = {'params': {'email': email}} + # A known email with a wrong password resolves to the existing user. + if email == os.getenv("valid_username"): + fields['matching_user_id'] = os.getenv("valid_user_id") + steps.append(run_step("filter", "$failed", fields)) - payload_to_castle = { - 'type': castle_type, - 'status': castle_status, - 'user': { - 'id': user_id, - 'email': email - }, - 'request_token': request_token - } - - if registered_at: - payload_to_castle["user"]["registered_at"] = registered_at - - castle = Client.from_request(request) - - if castle_api_endpoint == "risk": - verdict = castle.risk(payload_to_castle) - - elif castle_api_endpoint == "filter": - verdict = castle.filter(payload_to_castle) - - print("verdict:") - print(verdict) - - r = { - "api_endpoint": castle_api_endpoint, - "payload_to_castle": payload_to_castle, - "result": verdict, - "castle_type": castle_type, - "castle_status": castle_status - } - - return r, 200, {'ContentType':'application/json'} + return {"steps": steps}, 200, {'ContentType': 'application/json'} ################################# # Risk (profile update) diff --git a/demo_config.py b/demo_config.py index 395ce81..99fd920 100644 --- a/demo_config.py +++ b/demo_config.py @@ -6,11 +6,11 @@ demos = { "signup": { "friendly_name": "sign up", - "blurb": "Evaluate a registration ($registration) with the risk endpoint." + "blurb": "Filter a registration ($registration) before the account exists." }, "login": { "friendly_name": "login", - "blurb": "Evaluate a login with the risk and filter endpoints.", + "blurb": "Filter the attempt, then assess a successful login with Risk.", "wsd": "https://www.websequencediagrams.com/files/render?link=Q9WYp8rNThVZhA1inf2FSLfjChYZTdHXyGB9zqvMNpsaAvKvJPARgo5LI5fM5K4D" }, "account": { diff --git a/readme.md b/readme.md index 43ba6bc..23a1833 100644 --- a/readme.md +++ b/readme.md @@ -9,8 +9,8 @@ The app walks through a full user lifecycle. Every action mints a fresh Castle request token in the browser (`Castle.createRequestToken()`) and forwards it to the backend, which calls Castle and acts on the verdict. -- **sign up** – `$registration` to `risk` (a new email) or `filter` (an email that already exists) -- **login** – `$login` to `risk` (successful) or `filter` (failed) +- **sign up** – `$registration` to `filter` (anonymous, so the email goes in `params`): `$attempted` for a new email, `$failed` (resolved via `matching_user_id`) for an email that already exists +- **login** – `$login` reusing one request token across two calls: `filter` `$attempted` first, then `risk` `$succeeded` on success or `filter` `$failed` (wrong password / unknown user) - **account** – post-login actions: profile update (`$profile_update` to `risk`), a custom event (`Castle.custom()`), and logout (`$logout` via the non-blocking `log` endpoint) - **password reset** – `$password_reset` via the non-blocking `log` endpoint - **lists** – the Lists API (`create_list`, `get_all_lists`) diff --git a/static/app.js b/static/app.js index f1aea59..7318be6 100644 --- a/static/app.js +++ b/static/app.js @@ -96,3 +96,17 @@ function renderCastleResponse(data) { } showResultsCard(); } + +// Renders an ordered sequence of Castle calls (e.g. the login Filter -> Risk +// flow), one endpoint/payload/result block per step. +function renderCastleSteps(steps) { + clearResults(); + (steps || []).forEach(function (step) { + if (step.api_endpoint) addEndpointBadge(step.api_endpoint); + if (step.payload_to_castle) addJSONBlock("Payload sent to Castle", step.payload_to_castle); + if (step.result !== undefined && step.result !== null) { + addJSONBlock("Response from Castle", step.result); + } + }); + showResultsCard(); +} diff --git a/templates/login.html b/templates/login.html index cdea33f..acdc7f9 100644 --- a/templates/login.html +++ b/templates/login.html @@ -30,11 +30,11 @@ {% block desc %} -
A login attempt has three common outcomes, and each maps to a different Castle endpoint:
+A login reuses one request token across a two-step sequence:
$login / $attempted sent to /filter (anonymous, so the email goes in params).$login / $succeeded sent to /risk; act on the verdict (allow, challenge, deny).$login / $failed sent to /filter.$login / $failed (user id = null) sent to /filter.$login / $failed sent to /filter.A registration is a sensitive action, so it is risk-assessed:
+A registration is evaluated before the account exists, so it is anonymous activity sent to /filter with the form params:
$registration / $succeeded sent to /risk; act on the verdict (allow, challenge, deny).$registration / $failed sent to /filter.$registration / $attempted; act on the verdict (allow, challenge, deny) before creating the account.$registration / $failed (resolved to the existing user via matching_user_id).