locked-rb 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +127 -0
- data/lib/locked-rb.rb +3 -0
- data/lib/locked.rb +60 -0
- data/lib/locked/api.rb +40 -0
- data/lib/locked/api/request.rb +37 -0
- data/lib/locked/api/request/build.rb +29 -0
- data/lib/locked/api/response.rb +40 -0
- data/lib/locked/client.rb +66 -0
- data/lib/locked/command.rb +5 -0
- data/lib/locked/commands/authenticate.rb +23 -0
- data/lib/locked/commands/identify.rb +23 -0
- data/lib/locked/commands/review.rb +14 -0
- data/lib/locked/configuration.rb +75 -0
- data/lib/locked/context/default.rb +40 -0
- data/lib/locked/context/merger.rb +14 -0
- data/lib/locked/context/sanitizer.rb +23 -0
- data/lib/locked/errors.rb +41 -0
- data/lib/locked/extractors/client_id.rb +17 -0
- data/lib/locked/extractors/headers.rb +24 -0
- data/lib/locked/extractors/ip.rb +18 -0
- data/lib/locked/failover_auth_response.rb +23 -0
- data/lib/locked/header_formatter.rb +9 -0
- data/lib/locked/review.rb +11 -0
- data/lib/locked/secure_mode.rb +11 -0
- data/lib/locked/support/hanami.rb +19 -0
- data/lib/locked/support/padrino.rb +19 -0
- data/lib/locked/support/rails.rb +13 -0
- data/lib/locked/support/sinatra.rb +19 -0
- data/lib/locked/utils.rb +55 -0
- data/lib/locked/utils/cloner.rb +11 -0
- data/lib/locked/utils/merger.rb +23 -0
- data/lib/locked/utils/timestamp.rb +12 -0
- data/lib/locked/validators/not_supported.rb +16 -0
- data/lib/locked/validators/present.rb +16 -0
- data/lib/locked/version.rb +5 -0
- data/spec/lib/Locked/api/request/build_spec.rb +42 -0
- data/spec/lib/Locked/api/request_spec.rb +59 -0
- data/spec/lib/Locked/api/response_spec.rb +58 -0
- data/spec/lib/Locked/api_spec.rb +37 -0
- data/spec/lib/Locked/client_spec.rb +226 -0
- data/spec/lib/Locked/command_spec.rb +9 -0
- data/spec/lib/Locked/commands/authenticate_spec.rb +95 -0
- data/spec/lib/Locked/commands/identify_spec.rb +87 -0
- data/spec/lib/Locked/commands/review_spec.rb +24 -0
- data/spec/lib/Locked/configuration_spec.rb +146 -0
- data/spec/lib/Locked/context/default_spec.rb +35 -0
- data/spec/lib/Locked/context/merger_spec.rb +23 -0
- data/spec/lib/Locked/context/sanitizer_spec.rb +27 -0
- data/spec/lib/Locked/extractors/client_id_spec.rb +62 -0
- data/spec/lib/Locked/extractors/headers_spec.rb +26 -0
- data/spec/lib/Locked/extractors/ip_spec.rb +27 -0
- data/spec/lib/Locked/header_formatter_spec.rb +25 -0
- data/spec/lib/Locked/review_spec.rb +19 -0
- data/spec/lib/Locked/secure_mode_spec.rb +9 -0
- data/spec/lib/Locked/utils/cloner_spec.rb +18 -0
- data/spec/lib/Locked/utils/merger_spec.rb +13 -0
- data/spec/lib/Locked/utils/timestamp_spec.rb +17 -0
- data/spec/lib/Locked/utils_spec.rb +156 -0
- data/spec/lib/Locked/validators/not_supported_spec.rb +26 -0
- data/spec/lib/Locked/validators/present_spec.rb +33 -0
- data/spec/lib/Locked/version_spec.rb +5 -0
- data/spec/lib/locked_spec.rb +66 -0
- data/spec/spec_helper.rb +22 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 948c461ae63f6cbe1340d7f915b33a8bc335c9dd422134c4796204b1c433c677
|
4
|
+
data.tar.gz: 653750e9995e9532a458092143792d0cf020b6c898928b18adff742d6d80c2d6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b3a60f269cd41482a7509805a6d5317371bb8fd89b9bb2c98daef432c2726e7fc3c474f95e34e21b125ad489a496108cef14f7af747623c9a299107f841930ea
|
7
|
+
data.tar.gz: 9c6640642b6742a18a3347463490b47d8d458fded13d9ef0f2df9963da56ffb43e43315b584a4d233d7a8f25f54aad4ce7343f61a966f867cbd3153363ae5219
|
data/README.md
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# locked-ruby
|
2
|
+
|
3
|
+
[Locked](https://locked.jp) analyzes device, location, and interaction patterns in your web and mobile apps and lets you stop account takeover attacks in real-time..
|
4
|
+
|
5
|
+
### Installation
|
6
|
+
|
7
|
+
Add the `locked-rb` gem to your `Gemfile`
|
8
|
+
|
9
|
+
```
|
10
|
+
gem 'locked-rb'
|
11
|
+
```
|
12
|
+
|
13
|
+
### Configuration
|
14
|
+
|
15
|
+
**Framework configuration**
|
16
|
+
|
17
|
+
Load and configure the library with your Locked API key in an initializer or similar.
|
18
|
+
|
19
|
+
```
|
20
|
+
Locked.api_key = 'YOUR_API_KEY'
|
21
|
+
```
|
22
|
+
|
23
|
+
A Locked client instance will be made available as `locked` in your
|
24
|
+
|
25
|
+
* Rails controllers when you add require 'locked/support/rails'
|
26
|
+
|
27
|
+
* Padrino controllers when you add require 'locked/support/padrino'
|
28
|
+
|
29
|
+
* Sinatra app when you add `require 'locked/support/sinatra'` (and additionally explicitly add register `Sinatra::Locked` to your `Sinatra::Base` class if you have a modular application)
|
30
|
+
|
31
|
+
```
|
32
|
+
require 'locked/support/sinatra'
|
33
|
+
|
34
|
+
class ApplicationController < Sinatra::Base
|
35
|
+
register Sinatra::Locked
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
* Hanami when you add require 'locked/support/hanami' and include Locked::Hanami to your Hanami application
|
40
|
+
require 'locked/support/hanami'
|
41
|
+
|
42
|
+
```
|
43
|
+
module Web
|
44
|
+
class Application < Hanami::Application
|
45
|
+
include Locked::Hanami
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
### Client configuration
|
51
|
+
Configure the library in an initializer or similar.
|
52
|
+
```
|
53
|
+
Locked.configure do |config|
|
54
|
+
# Same as setting it through Locked.api_key
|
55
|
+
config.api_key = 'key'
|
56
|
+
|
57
|
+
# For authenticate method you can set failover strategies: deny (default), allow, verify, throw
|
58
|
+
config.failover_strategy = :deny
|
59
|
+
|
60
|
+
# Locked::RequestError is raised when timing out in milliseconds (default: 1000 milliseconds)
|
61
|
+
config.request_timeout = 2000
|
62
|
+
|
63
|
+
# Whitelisted and Blacklisted headers are case insensitive and allow to use _ and - as a separator, http prefixes are removed
|
64
|
+
# Whitelisted headers
|
65
|
+
config.whitelisted = ['X_HEADER']
|
66
|
+
# or append to default
|
67
|
+
config.whitelisted += ['http-x-header']
|
68
|
+
|
69
|
+
# Blacklisted headers take advantage over whitelisted elements
|
70
|
+
config.blacklisted = ['HTTP-X-header']
|
71
|
+
# or append to default
|
72
|
+
config.blacklisted += ['X_HEADER']
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
### Authenticating login request
|
77
|
+
|
78
|
+
Here is a simple example of authenticating request. The method `locked` is already included in Rails controllers.
|
79
|
+
```
|
80
|
+
begin
|
81
|
+
locked.authenticate(
|
82
|
+
event: '$login.success',
|
83
|
+
user_id: 1234,
|
84
|
+
user_ip: '1.1.1.1',
|
85
|
+
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063',
|
86
|
+
email: 'example@locked.jp',
|
87
|
+
callback_url: 'https://locked.jp/login/result'
|
88
|
+
)
|
89
|
+
rescue Locked::Error => e
|
90
|
+
puts e.message
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
When using plain Ruby, configure the `Locked` module and initiate a `Locked::Client` instance to use `authenticate` method
|
95
|
+
```
|
96
|
+
require 'locked-rb'
|
97
|
+
Locked.configure do |config|
|
98
|
+
config.api_key = 'key'
|
99
|
+
end
|
100
|
+
client = Locked::Client.new({})
|
101
|
+
client.authenticate(
|
102
|
+
event: '$login.success',
|
103
|
+
user_id: 1234,
|
104
|
+
user_ip: '1.1.1.1',
|
105
|
+
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063',
|
106
|
+
email: 'example@locked.jp',
|
107
|
+
callback_url: 'https://locked.jp/login/result'
|
108
|
+
)
|
109
|
+
```
|
110
|
+
|
111
|
+
And a possible response
|
112
|
+
```
|
113
|
+
{
|
114
|
+
success: true,
|
115
|
+
status: 200,
|
116
|
+
data: {
|
117
|
+
action: verify,
|
118
|
+
verify_token: 'f7e11d023c78'
|
119
|
+
}
|
120
|
+
}
|
121
|
+
```
|
122
|
+
|
123
|
+
When a user is required to verify his/her login, a `verify_token` is return so that later on your system can identify that user's login session.
|
124
|
+
|
125
|
+
### Exceptions
|
126
|
+
|
127
|
+
`Locked::Error` will be thrown if the Locked API returns a 400 or a 500 level HTTP response.
|
data/lib/locked-rb.rb
ADDED
data/lib/locked.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
%w[
|
4
|
+
openssl
|
5
|
+
net/http
|
6
|
+
json
|
7
|
+
time
|
8
|
+
].each(&method(:require))
|
9
|
+
|
10
|
+
%w[
|
11
|
+
locked/version
|
12
|
+
locked/errors
|
13
|
+
locked/command
|
14
|
+
locked/utils
|
15
|
+
locked/utils/merger
|
16
|
+
locked/utils/cloner
|
17
|
+
locked/utils/timestamp
|
18
|
+
locked/validators/present
|
19
|
+
locked/validators/not_supported
|
20
|
+
locked/context/merger
|
21
|
+
locked/context/sanitizer
|
22
|
+
locked/context/default
|
23
|
+
locked/commands/identify
|
24
|
+
locked/commands/authenticate
|
25
|
+
locked/commands/review
|
26
|
+
locked/configuration
|
27
|
+
locked/failover_auth_response
|
28
|
+
locked/client
|
29
|
+
locked/header_formatter
|
30
|
+
locked/secure_mode
|
31
|
+
locked/extractors/client_id
|
32
|
+
locked/extractors/headers
|
33
|
+
locked/extractors/ip
|
34
|
+
locked/api/response
|
35
|
+
locked/api/request
|
36
|
+
locked/api/request/build
|
37
|
+
locked/review
|
38
|
+
locked/api
|
39
|
+
].each(&method(:require))
|
40
|
+
|
41
|
+
# main sdk module
|
42
|
+
module Locked
|
43
|
+
class << self
|
44
|
+
def configure(config_hash = nil)
|
45
|
+
(config_hash || {}).each do |config_name, config_value|
|
46
|
+
config.send("#{config_name}=", config_value)
|
47
|
+
end
|
48
|
+
|
49
|
+
yield(config) if block_given?
|
50
|
+
end
|
51
|
+
|
52
|
+
def config
|
53
|
+
@configuration ||= Locked::Configuration.new
|
54
|
+
end
|
55
|
+
|
56
|
+
def api_key=(api_key)
|
57
|
+
config.api_key = api_key
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/locked/api.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Locked
|
4
|
+
# this class is responsible for making requests to api
|
5
|
+
module API
|
6
|
+
# Errors we handle internally
|
7
|
+
HANDLED_ERRORS = [
|
8
|
+
Timeout::Error,
|
9
|
+
Errno::EINVAL,
|
10
|
+
Errno::ECONNRESET,
|
11
|
+
EOFError,
|
12
|
+
Net::HTTPBadResponse,
|
13
|
+
Net::HTTPHeaderSyntaxError,
|
14
|
+
Net::ProtocolError
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
private_constant :HANDLED_ERRORS
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def request(command, headers = {})
|
21
|
+
raise Locked::ConfigurationError, 'configuration is not valid' unless Locked.config.valid?
|
22
|
+
|
23
|
+
begin
|
24
|
+
Locked::API::Response.call(
|
25
|
+
Locked::API::Request.call(
|
26
|
+
command,
|
27
|
+
Locked.config.api_key,
|
28
|
+
headers
|
29
|
+
)
|
30
|
+
)
|
31
|
+
rescue *HANDLED_ERRORS => error
|
32
|
+
# @note We need to initialize the error, as the original error is a cause for this
|
33
|
+
# custom exception. If we would do it the default Ruby way, the original error
|
34
|
+
# would get converted into a string
|
35
|
+
raise Locked::RequestError.new(error) # rubocop:disable Style/RaiseArgs
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Locked
|
4
|
+
# this class is responsible for making requests to api
|
5
|
+
module API
|
6
|
+
module Request
|
7
|
+
# Default headers that we add to passed ones
|
8
|
+
DEFAULT_HEADERS = {
|
9
|
+
'Content-Type' => 'application/json'
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
private_constant :DEFAULT_HEADERS
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def call(command, api_key, headers)
|
16
|
+
http.request(
|
17
|
+
Locked::API::Request::Build.call(
|
18
|
+
command,
|
19
|
+
headers.merge(DEFAULT_HEADERS),
|
20
|
+
api_key
|
21
|
+
)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def http
|
26
|
+
http = Net::HTTP.new(Locked.config.host, Locked.config.port)
|
27
|
+
http.read_timeout = Locked.config.request_timeout / 1000.0
|
28
|
+
if Locked.config.port == 443
|
29
|
+
http.use_ssl = true
|
30
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
31
|
+
end
|
32
|
+
http
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Locked
|
4
|
+
module API
|
5
|
+
# generate api request
|
6
|
+
module Request
|
7
|
+
module Build
|
8
|
+
class << self
|
9
|
+
API_KEY_HEADER = 'X-LOCKED-API-KEY'
|
10
|
+
def call(command, headers, api_key)
|
11
|
+
headers[API_KEY_HEADER] = api_key
|
12
|
+
request = Net::HTTP.const_get(
|
13
|
+
command.method.to_s.capitalize
|
14
|
+
).new("/#{Locked.config.url_prefix}/#{command.path}", headers)
|
15
|
+
|
16
|
+
command.data.delete(:context) # TODO: use context in request
|
17
|
+
unless command.method == :get
|
18
|
+
request.body = ::Locked::Utils.replace_invalid_characters(
|
19
|
+
command.data
|
20
|
+
).to_json
|
21
|
+
end
|
22
|
+
|
23
|
+
request
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Locked
|
4
|
+
module API
|
5
|
+
# parses api response
|
6
|
+
module Response
|
7
|
+
RESPONSE_ERRORS = {
|
8
|
+
400 => Locked::BadRequestError,
|
9
|
+
401 => Locked::UnauthorizedError,
|
10
|
+
403 => Locked::ForbiddenError,
|
11
|
+
404 => Locked::NotFoundError,
|
12
|
+
419 => Locked::UserUnauthorizedError,
|
13
|
+
422 => Locked::InvalidParametersError
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def call(response)
|
18
|
+
verify!(response)
|
19
|
+
|
20
|
+
return {} if response.body.nil? || response.body.empty?
|
21
|
+
|
22
|
+
begin
|
23
|
+
JSON.parse(response.body, symbolize_names: true)
|
24
|
+
rescue JSON::ParserError
|
25
|
+
raise Locked::ApiError, 'Invalid response from Locked API'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def verify!(response)
|
30
|
+
return if response.code.to_i.between?(200, 299)
|
31
|
+
|
32
|
+
raise Locked::InternalServerError if response.code.to_i.between?(500, 599)
|
33
|
+
|
34
|
+
error = RESPONSE_ERRORS.fetch(response.code.to_i, Locked::ApiError)
|
35
|
+
raise error, response[:message]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Locked
|
4
|
+
class Client
|
5
|
+
class << self
|
6
|
+
def from_request(request, options = {})
|
7
|
+
new(
|
8
|
+
to_context(request, options),
|
9
|
+
to_options(options)
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_context(request, options = {})
|
14
|
+
default_context = Locked::Context::Default.new(request, options[:cookies]).call
|
15
|
+
Locked::Context::Merger.call(default_context, options[:context])
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_options(options = {})
|
19
|
+
options[:timestamp] ||= Locked::Utils::Timestamp.call
|
20
|
+
warn '[DEPRECATION] use user_traits instead of traits key' if options.key?(:traits)
|
21
|
+
options
|
22
|
+
end
|
23
|
+
|
24
|
+
def failover_response_or_raise(failover_response, error)
|
25
|
+
return failover_response.generate unless Locked.config.failover_strategy == :throw
|
26
|
+
raise error
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_accessor :context
|
31
|
+
|
32
|
+
def initialize(context, options = {})
|
33
|
+
@timestamp = options[:timestamp]
|
34
|
+
@context = context
|
35
|
+
end
|
36
|
+
|
37
|
+
def authenticate(options = {})
|
38
|
+
options = Locked::Utils.deep_symbolize_keys(options || {})
|
39
|
+
|
40
|
+
add_timestamp_if_necessary(options)
|
41
|
+
command = Locked::Commands::Authenticate.new(@context).build(options)
|
42
|
+
begin
|
43
|
+
Locked::API.request(command).merge(failover: false, failover_reason: nil)
|
44
|
+
rescue Locked::RequestError, Locked::InternalServerError => error
|
45
|
+
self.class.failover_response_or_raise(
|
46
|
+
FailoverAuthResponse.new(options[:user_id], reason: error.to_s), error
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def identify(options = {})
|
52
|
+
options = Locked::Utils.deep_symbolize_keys(options || {})
|
53
|
+
|
54
|
+
add_timestamp_if_necessary(options)
|
55
|
+
|
56
|
+
command = Locked::Commands::Identify.new(@context).build(options)
|
57
|
+
Locked::API.request(command)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def add_timestamp_if_necessary(options)
|
63
|
+
options[:timestamp] ||= @timestamp if @timestamp
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Locked
|
4
|
+
module Commands
|
5
|
+
class Authenticate
|
6
|
+
def initialize(context)
|
7
|
+
@context = context
|
8
|
+
end
|
9
|
+
|
10
|
+
def build(options = {})
|
11
|
+
Locked::Validators::Present.call(options, %i[event])
|
12
|
+
context = Locked::Context::Merger.call(@context, options[:context])
|
13
|
+
context = Locked::Context::Sanitizer.call(context)
|
14
|
+
|
15
|
+
Locked::Command.new(
|
16
|
+
'authenticate',
|
17
|
+
options.merge(context: context),
|
18
|
+
:post
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|