locked-rb 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|