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
129 changes: 68 additions & 61 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ function buildApp(castle = require('./castle')) {
};

// a default value reused across the login / password-reset demos
let registeredAt = '2020-02-23T22:28:55.387Z';
const registeredAt = '2020-02-23T22:28:55.387Z';

// -------------------------------------------------------------------------
// Page routes
Expand Down Expand Up @@ -203,41 +203,42 @@ function buildApp(castle = require('./castle')) {
});

// -------------------------------------------------------------------------
// Risk / Filter (registration)
// Filter (registration)
// -------------------------------------------------------------------------

// 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.
app.post('/evaluate_signup', async (req, res) => {
const { name, email, request_token } = req.body;
const { email, request_token } = req.body;

const castleType = '$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.
const alreadyRegistered = email === process.env.valid_username;
const castleStatus = alreadyRegistered ? '$failed' : '$succeeded';
const apiEndpoint = alreadyRegistered ? 'filter' : 'risk';
const castleStatus = alreadyRegistered ? '$failed' : '$attempted';

const payloadToCastle = {
type: castleType,
status: castleStatus,
user: { id: process.env.valid_user_id, email, name },
params: { email },
request_token,
context: buildContext(req),
};
if (alreadyRegistered) {
payloadToCastle.matching_user_id = process.env.valid_user_id;
}

let result;
try {
result =
apiEndpoint === 'risk'
? await castle.risk(payloadToCastle)
: await castle.filter(payloadToCastle);
result = await castle.filter(payloadToCastle);
} catch (err) {
result = errorResult(err);
}

const { context, ...echoedPayload } = payloadToCastle;

res.json({
api_endpoint: apiEndpoint,
api_endpoint: 'filter',
payload_to_castle: echoedPayload,
result,
castle_type: castleType,
Expand All @@ -246,67 +247,73 @@ function buildApp(castle = require('./castle')) {
});

// -------------------------------------------------------------------------
// Risk / Filter (login)
// Filter -> Risk (login)
// -------------------------------------------------------------------------

// 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.
app.post('/evaluate_login', async (req, res) => {
const { email, password, request_token } = req.body;

const castleType = '$login';
let userId;
let castleStatus;
let apiEndpoint;

// check validity of username + password combo
if (email === process.env.valid_username) {
userId = process.env.valid_user_id;

if (password === process.env.valid_password) {
castleStatus = '$succeeded';
apiEndpoint = 'risk';
} else {
castleStatus = '$failed';
apiEndpoint = 'filter';
const runStep = async (apiEndpoint, castleStatus, fields) => {
const payloadToCastle = {
type: castleType,
status: castleStatus,
...fields,
request_token,
context: buildContext(req),
};

let result;
try {
result =
apiEndpoint === 'risk'
? await castle.risk(payloadToCastle)
: await castle.filter(payloadToCastle);
} catch (err) {
result = errorResult(err);
}
} else {
apiEndpoint = 'filter';
castleStatus = '$failed';
userId = null;
registeredAt = null;
}

const payloadToCastle = {
type: castleType,
status: castleStatus,
user: { id: userId, email },
request_token,
context: buildContext(req),
// context is large and noisy; don't echo it back to the browser.
const { context, ...echoedPayload } = payloadToCastle;
return {
api_endpoint: apiEndpoint,
payload_to_castle: echoedPayload,
result,
castle_type: castleType,
castle_status: castleStatus,
};
};

if (registeredAt) {
payloadToCastle.user.registered_at = registeredAt;
}
// Step 1 — always filter the attempt up front (anonymous -> params).
const steps = [await runStep('filter', '$attempted', { params: { email } })];

let result;
try {
result =
apiEndpoint === 'risk'
? await castle.risk(payloadToCastle)
: await castle.filter(payloadToCastle);
} catch (err) {
result = errorResult(err);
// Step 2 — the outcome, on the same request token.
if (
email === process.env.valid_username &&
password === process.env.valid_password
) {
steps.push(
await runStep('risk', '$succeeded', {
user: {
id: process.env.valid_user_id,
email,
registered_at: registeredAt,
},
})
);
} else {
const fields = { params: { email } };
// A known email with a wrong password resolves to the existing user.
if (email === process.env.valid_username) {
fields.matching_user_id = process.env.valid_user_id;
}
steps.push(await runStep('filter', '$failed', fields));
}

// context is large and noisy; don't echo it back to the browser.
const { context, ...echoedPayload } = payloadToCastle;

res.json({
api_endpoint: apiEndpoint,
payload_to_castle: echoedPayload,
result,
castle_type: castleType,
castle_status: castleStatus,
});
res.json({ steps });
});

// -------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions demo_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
const 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',
},
password_reset: {
Expand Down
30 changes: 0 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ the backend, which calls Castle and acts on the verdict.

Server-rendered pages:

- **sign up** – `$registration` to `risk` (a new email) or `filter` (an email that already exists)
- **login** – `$login` to `risk` (successful) or `filter` (failed), with the verdict (allow / challenge / deny), risk score and signals surfaced in the UI
- **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); the verdict (allow / challenge / deny), risk score and signals are surfaced per step
- **password reset** – `$password_reset` via the non-blocking `log` endpoint
- **lists** – the Lists API (`createList`, `fetchAllLists`)
- **privacy** – the Privacy API (`requestUserData`, `deleteUserData`)
Expand Down
15 changes: 15 additions & 0 deletions static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,21 @@ function renderCastleResponse(data) {
showResultsCard();
}

// Renders an ordered sequence of Castle calls (e.g. the login Filter -> Risk
// flow), one endpoint/verdict/payload/result block per step.
function renderCastleSteps(steps) {
clearResults();
(steps || []).forEach(function (step) {
if (step.api_endpoint) addEndpointBadge(step.api_endpoint);
addVerdictBanner(step.result);
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();
}

// Tell Castle which page the user is on. Safe no-op if the browser SDK or the
// publishable key is unavailable.
function trackPage() {
Expand Down
Loading
Loading