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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +127 -0
  3. data/lib/locked-rb.rb +3 -0
  4. data/lib/locked.rb +60 -0
  5. data/lib/locked/api.rb +40 -0
  6. data/lib/locked/api/request.rb +37 -0
  7. data/lib/locked/api/request/build.rb +29 -0
  8. data/lib/locked/api/response.rb +40 -0
  9. data/lib/locked/client.rb +66 -0
  10. data/lib/locked/command.rb +5 -0
  11. data/lib/locked/commands/authenticate.rb +23 -0
  12. data/lib/locked/commands/identify.rb +23 -0
  13. data/lib/locked/commands/review.rb +14 -0
  14. data/lib/locked/configuration.rb +75 -0
  15. data/lib/locked/context/default.rb +40 -0
  16. data/lib/locked/context/merger.rb +14 -0
  17. data/lib/locked/context/sanitizer.rb +23 -0
  18. data/lib/locked/errors.rb +41 -0
  19. data/lib/locked/extractors/client_id.rb +17 -0
  20. data/lib/locked/extractors/headers.rb +24 -0
  21. data/lib/locked/extractors/ip.rb +18 -0
  22. data/lib/locked/failover_auth_response.rb +23 -0
  23. data/lib/locked/header_formatter.rb +9 -0
  24. data/lib/locked/review.rb +11 -0
  25. data/lib/locked/secure_mode.rb +11 -0
  26. data/lib/locked/support/hanami.rb +19 -0
  27. data/lib/locked/support/padrino.rb +19 -0
  28. data/lib/locked/support/rails.rb +13 -0
  29. data/lib/locked/support/sinatra.rb +19 -0
  30. data/lib/locked/utils.rb +55 -0
  31. data/lib/locked/utils/cloner.rb +11 -0
  32. data/lib/locked/utils/merger.rb +23 -0
  33. data/lib/locked/utils/timestamp.rb +12 -0
  34. data/lib/locked/validators/not_supported.rb +16 -0
  35. data/lib/locked/validators/present.rb +16 -0
  36. data/lib/locked/version.rb +5 -0
  37. data/spec/lib/Locked/api/request/build_spec.rb +42 -0
  38. data/spec/lib/Locked/api/request_spec.rb +59 -0
  39. data/spec/lib/Locked/api/response_spec.rb +58 -0
  40. data/spec/lib/Locked/api_spec.rb +37 -0
  41. data/spec/lib/Locked/client_spec.rb +226 -0
  42. data/spec/lib/Locked/command_spec.rb +9 -0
  43. data/spec/lib/Locked/commands/authenticate_spec.rb +95 -0
  44. data/spec/lib/Locked/commands/identify_spec.rb +87 -0
  45. data/spec/lib/Locked/commands/review_spec.rb +24 -0
  46. data/spec/lib/Locked/configuration_spec.rb +146 -0
  47. data/spec/lib/Locked/context/default_spec.rb +35 -0
  48. data/spec/lib/Locked/context/merger_spec.rb +23 -0
  49. data/spec/lib/Locked/context/sanitizer_spec.rb +27 -0
  50. data/spec/lib/Locked/extractors/client_id_spec.rb +62 -0
  51. data/spec/lib/Locked/extractors/headers_spec.rb +26 -0
  52. data/spec/lib/Locked/extractors/ip_spec.rb +27 -0
  53. data/spec/lib/Locked/header_formatter_spec.rb +25 -0
  54. data/spec/lib/Locked/review_spec.rb +19 -0
  55. data/spec/lib/Locked/secure_mode_spec.rb +9 -0
  56. data/spec/lib/Locked/utils/cloner_spec.rb +18 -0
  57. data/spec/lib/Locked/utils/merger_spec.rb +13 -0
  58. data/spec/lib/Locked/utils/timestamp_spec.rb +17 -0
  59. data/spec/lib/Locked/utils_spec.rb +156 -0
  60. data/spec/lib/Locked/validators/not_supported_spec.rb +26 -0
  61. data/spec/lib/Locked/validators/present_spec.rb +33 -0
  62. data/spec/lib/Locked/version_spec.rb +5 -0
  63. data/spec/lib/locked_spec.rb +66 -0
  64. data/spec/spec_helper.rb +22 -0
  65. 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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'locked'
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Locked
4
+ Command = Struct.new(:path, :data, :method)
5
+ 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