locked-rb 0.0.1

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