Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 63 additions & 77 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,121 +128,107 @@ 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,
"castle_status": castle_status,
}, 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)
Expand Down
4 changes: 2 additions & 2 deletions demo_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
14 changes: 14 additions & 0 deletions static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
14 changes: 9 additions & 5 deletions templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@

{% block desc %}

<p>A login attempt has three common outcomes, and each maps to a different Castle endpoint:</p>
<p>A login reuses one request token across a two-step sequence:</p>
<ol class="list-decimal pl-5 space-y-1">
<li>the attempt is always filtered first &rarr; <code>$login / $attempted</code> sent to <code>/filter</code> (anonymous, so the email goes in <code>params</code>).</li>
<li><strong>valid username + valid password</strong> &rarr; <code>$login / $succeeded</code> sent to <code>/risk</code>; act on the verdict (allow, challenge, deny).</li>
<li><strong>valid username + invalid password</strong> &rarr; <code>$login / $failed</code> sent to <code>/filter</code>.</li>
<li><strong>invalid username</strong> &rarr; <code>$login / $failed</code> (user id = null) sent to <code>/filter</code>.</li>
<li><strong>wrong password / unknown user</strong> &rarr; <code>$login / $failed</code> sent to <code>/filter</code>.</li>
</ol>

{% endblock %}
Expand Down Expand Up @@ -67,8 +67,12 @@
password: document.getElementById("password").value,
request_token: requestToken,
}).then(function (data) {
renderCastleResponse(data);
const action = data.result && data.result.policy && data.result.policy.action;
renderCastleSteps(data.steps);
// Act on the outcome (the last step): a /risk allow lets the
// user continue to their account.
const steps = data.steps || [];
const last = steps[steps.length - 1];
const action = last && last.result && last.result.policy && last.result.policy.action;
if (action === "allow") {
const results = document.getElementById("results");
const wrap = document.createElement("div");
Expand Down
6 changes: 3 additions & 3 deletions templates/signup.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@

{% block desc %}

<p>A registration is a sensitive action, so it is risk-assessed:</p>
<p>A registration is evaluated before the account exists, so it is anonymous activity sent to <code>/filter</code> with the form <code>params</code>:</p>
<ol class="list-decimal pl-5 space-y-1">
<li><strong>a new email</strong> &rarr; <code>$registration / $succeeded</code> sent to <code>/risk</code>; act on the verdict (allow, challenge, deny).</li>
<li><strong>an email that already exists</strong> &rarr; <code>$registration / $failed</code> sent to <code>/filter</code>.</li>
<li><strong>a new email</strong> &rarr; <code>$registration / $attempted</code>; act on the verdict (allow, challenge, deny) before creating the account.</li>
<li><strong>an email that already exists</strong> &rarr; <code>$registration / $failed</code> (resolved to the existing user via <code>matching_user_id</code>).</li>
</ol>

{% endblock %}
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@

import app as app_module # noqa: E402 (must follow the env setup above)

# The known-good registration date the app uses as a module-level default. The
# `evaluate_login` handler mutates this global, so we restore it before each test.
# The known-good registration date the app uses as a module-level default. It is
# restored before each test so module state stays deterministic across the suite.
DEFAULT_REGISTERED_AT = "2020-02-23T22:28:55.387Z"


Expand Down
Loading
Loading