Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge develop -> master #128

Merged
merged 4 commits into from
Mar 2, 2022
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
128 changes: 4 additions & 124 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Castle SDK for Node

**[Castle](https://castle.io) analyzes device, location, and interaction patterns in your web and mobile apps and lets you stop account takeover attacks in real-time..**
**[Castle](https://castle.io) analyzes user behavior in web and mobile apps to stop fraud before it happens.**

## Documentation
## Usage

[Official Castle docs](https://castle.io/docs)
See the [documentation](https://docs.castle.io) for how to use this SDK with the Castle APIs

## Installation

Expand Down Expand Up @@ -40,7 +40,7 @@ const castle = new Castle({ apiSecret: 'YOUR SECRET HERE' });
| ----------------- | ----------- |
| apiSecret | `string` - This can be found in the Castle dashboard. |
| baseUrl | `string` - Base Castle API url. |
| timeout | `number` - Time before returning the failover strategy. Default value is 500. |
| timeout | `number` - Time before returning the failover strategy. Default value is 1000. |
| allowlisted | `string[]` - An array of strings matching the headers you want to pass fully to the service. We highly recommend using the DEFAULT_ALLOWLIST constant. |
| denylisted | `string[]` - An array of of strings matching the headers you do not want to pass fully to the service. |
| failoverStrategy | `FailoverStrategy` - If the request to our service would for some reason time out, this is where you select the automatic response from `authenticate`. Options are `FailoverStrategy.allow`, `FailoverStrategy.deny`, `FailoverStrategy.challenge`. |
Expand All @@ -51,123 +51,3 @@ const castle = new Castle({ apiSecret: 'YOUR SECRET HERE' });
| trustProxyChain | `boolean` - False by default, defines if trusting all of the proxy IPs in X-Forwarded-For is enabled. |
| trustedProxyDepth | `number` - Number of trusted proxies used in the chain. |

## Actions

The `castle` instance exposes two methods, `track` and `authenticate`. In order to provide the information required in both these methods, you'll need access to the logged in user information (if that is available at that stage in the user flow), as well as request information. In node connect/express, the user is often found in the `session` object, or directly on the `request` object in the `user` property, if you are using passport.

### track

This is the asynchronous version of the castle integration. This is for events where you don't require a response.

```js

castle.track({
event: '$profile_update.failed',
user_id: user.id,
user_traits: {
email: user.email,
registered_at: user.registered_at,
},
context: {
ip: request.ip,
client_id: request.cookies['__cid'],
headers: request.headers,
},
});
```

### authenticate

This is the synchronous version of the castle integration. This is for events where you require a response. It is used in the same way as `track`, except that you have the option of waiting for a response.

```js
let response;
try {
response = await castle.authenticate({
event: '$login.succeeded',
user_id: user.id,
user_traits: {
email: user.email,
registered_at: user.registered_at,
},
context: {
ip: request.ip,
client_id: request.cookies['__cid'],
headers: request.headers,
},
});
} catch (e) {
console.error(e);
}

console.log(response); // { "action": "allow", "user_id": 123, "device_token": "eyj...." }
```

####

Response format

| Response key | value |
| --------------- | --------------------------------------------------------------------------------------------------- |
| action | `string` - The recommended action for the given event. Options: `allow`, `challenge`, `deny`. |
| user_id | `string` - The `user_id` of the end user. |
| policy | `object` - object containing risk policy information, such as `id`,`revision_id`, `name` |
| signals | `object` - object containing hash with signals names |
| device_token | `string` - Our token for the device that generated the event. |
| failover | `boolean` - An optional property indicating the request failed and the response is a failover. |
| failover_reason | `string` - A message indicating why the request failed. |

### All config options for `track` and `authenticate`

| Config option | Explanation |
| ------------- | ----------- |
| event | `string` - The event generated by the user |
| user_id | `string` - The `user_id` of the end user. |
| user_traits | `object` - An optional, recommended, object containing user information, such as `email` and `registered_at`. |
| properties | `object` - An optional object containing custom information. |
| created_at | `string` - An optional ISO date string indicating when the event occurred, in cases where this might be different from the time when the request is made. |
| device_token | `string` - The optional device token, used for mitigating or escalating. |
| context | `object` - The request context information. |

## Device management

This SDK allows issuing requests to [Castle's Device Management Endpoints](https://docs.castle.io/device_management_tool/). Use these endpoints for admin-level management of end-user devices (i.e., for an internal dashboard).

Fetching device data, approving a device, reporting a device requires a valid `device_token`.

```js
// Get device data
castle.getDevice({ device_token })
// Approve a device
castle.approveDevice({ device_token })
// Report a device
castle.reportDevice({ device_token })
```

Fetching available devices that belong to a given user requires a valid `user_id`.

```js
// Get user's devices data
castle.getDevicesForUser({ user_id })
```

## Payload

To generate the payload, use the following command:
```js
const payload = PayloadPrepareService.call(
{
event: '$login.succeeded',
user_id: user.id,
properties: {
key: 'value'
},
user_traits: {
key: 'value'
}
},
request,
castle.configuration
)
castle.track(payload)
```
10 changes: 9 additions & 1 deletion src/core/services/core-process-response.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
NotFoundError,
UserUnauthorizedError,
InvalidParametersError,
InvalidRequestTokenError,
InternalServerError,
APIError,
} from '../../errors';
Expand All @@ -19,6 +20,10 @@ const RESPONSE_ERRORS = {
'422': InvalidParametersError,
};

const RESPONSE_SUB_ERRORS = {
'invalid_request_token': InvalidRequestTokenError
};

// The body on the request is a stream and can only be
// read once, by default. This is a workaround so that the
// logging functions can read the body independently
Expand Down Expand Up @@ -58,7 +63,10 @@ export const CoreProcessResponseService = {
);
}

const err = RESPONSE_ERRORS[response.status.toString()];
// Throw a special exception for subtype errors if defined. Eg. for
// invalid request token, which is a subtype of InvalidParametersError.
// Otherwise, throw exception as defined per status code
const err = RESPONSE_SUB_ERRORS[body.type] || RESPONSE_ERRORS[response.status.toString()];

throw new err(`Castle: Responded with ${response.status} code`);
},
Expand Down
8 changes: 8 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export class InvalidParametersError extends APIError {
}
}

// api error invalid param 422, subtype
export class InvalidRequestTokenError extends InvalidParametersError {
constructor(message: string) {
super(message);
this.name = 'InvalidRequestTokenError';
}
}

// all internal server errors
export class InternalServerError extends APIError {
constructor(message: string) {
Expand Down
19 changes: 19 additions & 0 deletions test/core/services/core-process-response.service.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CoreProcessResponseService } from '../../../src/core/core.module';
import { Response } from 'node-fetch';
import { InvalidRequestTokenError } from '../../../src/errors';

describe('CoreProcessResponseService', () => {
describe('call', () => {
Expand Down Expand Up @@ -180,6 +181,24 @@ describe('CoreProcessResponseService', () => {
});
});
});

describe('with invalid request token', () => {
const response = new Response(JSON.stringify({
type: "invalid_request_token",
message: "Invalid Request Token"
}), {
headers: { 'Content-Type': 'application/json' },
status: 422
})

it('throws InvalidRequestTokenError', async () => {
await expect(
CoreProcessResponseService.call('risk', {}, response, {
info: () => {},
})
).rejects.toThrow(InvalidRequestTokenError)
})
});
});
});
});