trusona 0.16.0

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 (84) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.env.example +4 -0
  5. data/.gitignore +20 -0
  6. data/.rspec +3 -0
  7. data/.ruby-version +1 -0
  8. data/.travis.yml +24 -0
  9. data/Gemfile +8 -0
  10. data/Guardfile +54 -0
  11. data/LICENSE +201 -0
  12. data/README.md +234 -0
  13. data/Rakefile +8 -0
  14. data/bin/console +21 -0
  15. data/bin/setup +8 -0
  16. data/certs/trusona.pem +26 -0
  17. data/checksum/trusona-0.16.0.gem.sha512 +1 -0
  18. data/integrations/device_user_binding_integration_spec.rb +6 -0
  19. data/integrations/identity_documents_spec.rb +28 -0
  20. data/integrations/spec_helper.rb +23 -0
  21. data/integrations/tru_code_spec.rb +80 -0
  22. data/integrations/trusonafication_spec.rb +61 -0
  23. data/integrations/user_accounts_spec.rb +39 -0
  24. data/integrations/user_identifiers_spec.rb +32 -0
  25. data/lib/trusona.rb +157 -0
  26. data/lib/trusona/api/client.rb +60 -0
  27. data/lib/trusona/api/hashed_message.rb +71 -0
  28. data/lib/trusona/api/signed_request.rb +96 -0
  29. data/lib/trusona/api/verified_response.rb +85 -0
  30. data/lib/trusona/device.rb +31 -0
  31. data/lib/trusona/device_user_binding.rb +56 -0
  32. data/lib/trusona/errors.rb +51 -0
  33. data/lib/trusona/helpers/key_normalizer.rb +15 -0
  34. data/lib/trusona/helpers/time_normalizer.rb +17 -0
  35. data/lib/trusona/identity_document.rb +64 -0
  36. data/lib/trusona/mappers/base_mapper.rb +69 -0
  37. data/lib/trusona/mappers/device_mapper.rb +13 -0
  38. data/lib/trusona/mappers/device_user_binding_mapper.rb +13 -0
  39. data/lib/trusona/mappers/identity_document_mapper.rb +17 -0
  40. data/lib/trusona/mappers/nil_mapper.rb +11 -0
  41. data/lib/trusona/mappers/tru_code_mapper.rb +13 -0
  42. data/lib/trusona/mappers/trusonafication_mapper.rb +19 -0
  43. data/lib/trusona/mappers/user_account_mapper.rb +13 -0
  44. data/lib/trusona/mappers/user_identifier_mapper.rb +13 -0
  45. data/lib/trusona/resources/base_resource.rb +29 -0
  46. data/lib/trusona/resources/device.rb +22 -0
  47. data/lib/trusona/resources/device_user_binding.rb +30 -0
  48. data/lib/trusona/resources/device_user_binding_activation.rb +27 -0
  49. data/lib/trusona/resources/identity_document.rb +36 -0
  50. data/lib/trusona/resources/tru_code.rb +42 -0
  51. data/lib/trusona/resources/trusonafication.rb +137 -0
  52. data/lib/trusona/resources/user_account.rb +84 -0
  53. data/lib/trusona/resources/user_identifier.rb +49 -0
  54. data/lib/trusona/resources/validators.rb +22 -0
  55. data/lib/trusona/services/account_lookups_service.rb +17 -0
  56. data/lib/trusona/services/base_service.rb +117 -0
  57. data/lib/trusona/services/device_user_bindings_service.rb +22 -0
  58. data/lib/trusona/services/devices_service.rb +15 -0
  59. data/lib/trusona/services/identity_documents_service.rb +31 -0
  60. data/lib/trusona/services/tru_codes_service.rb +26 -0
  61. data/lib/trusona/services/trusonafication_service.rb +16 -0
  62. data/lib/trusona/services/user_accounts_service.rb +17 -0
  63. data/lib/trusona/services/user_identifiers_service.rb +16 -0
  64. data/lib/trusona/tru_code.rb +38 -0
  65. data/lib/trusona/tru_code_config.rb +45 -0
  66. data/lib/trusona/trusonafication.rb +133 -0
  67. data/lib/trusona/user_account.rb +17 -0
  68. data/lib/trusona/user_identifier.rb +15 -0
  69. data/lib/trusona/version.rb +5 -0
  70. data/lib/trusona/workers/device_finder.rb +18 -0
  71. data/lib/trusona/workers/device_user_binding_activator.rb +25 -0
  72. data/lib/trusona/workers/device_user_binding_creator.rb +26 -0
  73. data/lib/trusona/workers/identity_document_finder.rb +25 -0
  74. data/lib/trusona/workers/tru_code_creator.rb +17 -0
  75. data/lib/trusona/workers/tru_code_finder.rb +19 -0
  76. data/lib/trusona/workers/trusonafication_creator.rb +46 -0
  77. data/lib/trusona/workers/trusonafication_finder.rb +27 -0
  78. data/lib/trusona/workers/user_account_finder.rb +39 -0
  79. data/lib/trusona/workers/user_identifier_creator.rb +17 -0
  80. data/lib/trusona/workers/user_identifier_finder.rb +29 -0
  81. data/release-gem +17 -0
  82. data/trusona.gemspec +43 -0
  83. metadata +333 -0
  84. metadata.gz.sig +1 -0
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trusona
4
+ module Api
5
+ #
6
+ ## An a wrapper around HTTParty
7
+ class HTTPClient
8
+ POST = 'POST'
9
+ GET = 'GET'
10
+ PATCH = 'PATCH'
11
+ CONTENT_TYPE = 'application/json;charset=utf-8'
12
+
13
+ def initialize(host = nil)
14
+ @host = host || Trusona.config.api_host
15
+ end
16
+
17
+ def post(path, params = {})
18
+ execute(path, params, POST)
19
+ end
20
+
21
+ def patch(path, params = {})
22
+ execute(path, params, PATCH)
23
+ end
24
+
25
+ def get(path, params = {})
26
+ execute(path, params, GET)
27
+ end
28
+
29
+ private
30
+
31
+ def execute(path, params, method)
32
+ request = Trusona::Api::SignedRequest.new(path, params, method, @host)
33
+
34
+ # Power of ruby or hard to read?
35
+ unverified = HTTParty.send(
36
+ method.downcase,
37
+ request.uri,
38
+ body: request.body,
39
+ headers: request.headers
40
+ )
41
+
42
+ Trusona::Api::VerifiedResponse.new(unverified)
43
+ end
44
+ end
45
+
46
+ ##
47
+ # A default nil http client
48
+ class NilHTTPClient
49
+ def initialize(host)
50
+ @host = host
51
+ end
52
+
53
+ def post(_uri, _params); end
54
+
55
+ def get(_uri, _params); end
56
+
57
+ def patch(_uri, _params); end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Trusona
6
+ module Api
7
+ #
8
+ ## A HMAC message suitable for authorization to the Trusona API
9
+ class HashedMessage
10
+ def initialize(params = {})
11
+ validate(params)
12
+
13
+ @method = params[:method]
14
+ @body = params[:body]
15
+ @content_type = params[:content_type]
16
+ @path = params[:path]
17
+ @date = params[:date]
18
+ @secret = Trusona.config.secret
19
+ @token = Trusona.config.token
20
+ end
21
+
22
+ def auth_header
23
+ "TRUSONA #{@token}:#{signature}"
24
+ end
25
+
26
+ def signature
27
+ Base64.strict_encode64(
28
+ OpenSSL::HMAC.hexdigest(
29
+ OpenSSL::Digest::SHA256.new, @secret, prepare_data
30
+ )
31
+ )
32
+ end
33
+
34
+ private
35
+
36
+ def body_digest
37
+ digestable_body = ''
38
+ digestable_body = @body unless @body.nil? || @body.empty?
39
+
40
+ OpenSSL::Digest::MD5.new.hexdigest(digestable_body)
41
+ end
42
+
43
+ def invalid_param?(param)
44
+ param.nil?
45
+ end
46
+
47
+ def invalid_method?(method)
48
+ http_methods = %w[GET POST DELETE PATCH PUT]
49
+ return true if invalid_param?(method)
50
+ return true unless http_methods.include?(method.strip.upcase)
51
+ end
52
+
53
+ def prepare_data
54
+ data = [
55
+ @method.to_s,
56
+ body_digest,
57
+ @content_type,
58
+ @date,
59
+ @path
60
+ ]
61
+
62
+ data.join("\n")
63
+ end
64
+
65
+ def validate(params)
66
+ raise ArgumentError if invalid_method?(params[:method])
67
+ raise ArgumentError if invalid_param?(params[:path])
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trusona
4
+ module Api
5
+ #
6
+ ## A signed request that can be used to make HMAC'd API calls to Trusona
7
+ class SignedRequest
8
+ attr_reader :uri, :body
9
+
10
+ def initialize(path, body, method, host)
11
+ @host = host
12
+ @uri = build_uri(path, body)
13
+ @body = parse_body(body)
14
+ @path = build_path(path)
15
+ @method = method
16
+ @headers = { 'x-date' => nil, 'Authorization' => nil }
17
+ @date = Time.now.httpdate
18
+ validate
19
+ @signature = sign
20
+ end
21
+
22
+ def headers
23
+ @headers.merge(
24
+ 'x-date' => @date,
25
+ 'Date' => @date,
26
+ 'X-Date' => @date,
27
+ 'Authorization' => @signature,
28
+ 'Content-Type' => determine_content_type
29
+ )
30
+ end
31
+
32
+ private
33
+
34
+ def determine_content_type
35
+ return '' if @method == 'GET' || @method == 'DELETE'
36
+ Trusona::Api::HTTPClient::CONTENT_TYPE
37
+ end
38
+
39
+ def build_path(path)
40
+ return path if @uri.query.nil? || @uri.query.empty?
41
+ [@uri.path, @uri.query].join('?')
42
+ end
43
+
44
+ def build_uri(path, body)
45
+ return build_uri_with_query(URI(path)) if URI(path).query
46
+ return build_uri_with_body_as_query(path, body) if valid_hash_body(body)
47
+ URI::HTTPS.build(host: @host, path: path)
48
+ end
49
+
50
+ def valid_hash_body(body)
51
+ body.is_a?(Hash) && !body.empty?
52
+ end
53
+
54
+ def parse_body(body)
55
+ body.is_a?(Hash) ? '' : body
56
+ end
57
+
58
+ def sign
59
+ message = Trusona::Api::HashedMessage.new(
60
+ body: @body,
61
+ content_type: determine_content_type,
62
+ path: @path,
63
+ method: @method,
64
+ date: @date
65
+ )
66
+
67
+ message.auth_header
68
+ rescue ArgumentError
69
+ raise Trusona::SigningError
70
+ end
71
+
72
+ def build_uri_with_query(generic)
73
+ URI::HTTPS.build(
74
+ host: @host,
75
+ path: generic.path,
76
+ query: generic.query
77
+ )
78
+ end
79
+
80
+ def build_uri_with_body_as_query(path, body)
81
+ URI::HTTPS.build(
82
+ host: @host,
83
+ path: path,
84
+ query: URI.encode_www_form(body)
85
+ )
86
+ end
87
+
88
+ def validate
89
+ raise ArgumentError unless @path
90
+ raise ArgumentError unless @body
91
+ raise ArgumentError unless @method
92
+ raise ArgumentError unless @host
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trusona
4
+ module Api
5
+ #
6
+ ## a response from the Trusona API that can be verified with HMAC
7
+ class VerifiedResponse
8
+ LEGACY_SERVER_HEADER = 'Apache-Coyote/1.1'
9
+ attr_reader :code
10
+
11
+ def initialize(unverified)
12
+ @unverified = unverified
13
+ @verified = verify
14
+ @code = unverified.code
15
+ end
16
+
17
+ def to_h
18
+ JSON.parse(@unverified.body)
19
+ end
20
+
21
+ def verified?
22
+ @verified
23
+ end
24
+
25
+ private
26
+
27
+ def verify
28
+ expected = expected_signature
29
+ actual = @unverified.headers['X-Signature']
30
+ actual == expected || actual == Base64.strict_decode64(expected)
31
+ end
32
+
33
+ # rubocop:disable Metrics/MethodLength
34
+ def expected_signature
35
+ begin
36
+ message = Trusona::Api::HashedMessage.new(
37
+ method: parse_method(@unverified.request.http_method),
38
+ body: @unverified.body,
39
+ content_type: determine_content_type,
40
+ path: parse_path(@unverified.request.uri),
41
+ date: parse_date(@unverified.headers)
42
+ )
43
+ rescue ArgumentError
44
+ raise Trusona::SigningError
45
+ end
46
+
47
+ message.signature
48
+ end
49
+ # rubocop:enable Metrics/MethodLength
50
+
51
+ def parse_path(uri)
52
+ return uri.path unless uri.query
53
+ [uri.path, uri.query].join('?')
54
+ end
55
+
56
+ def parse_date(headers)
57
+ headers['X-Date'] || headers['x-date'] || headers['Date']
58
+ end
59
+
60
+ def determine_content_type
61
+ server = @unverified.headers['server']
62
+ response_type = @unverified.headers['Content-Type']
63
+ return response_type unless server == LEGACY_SERVER_HEADER
64
+ determine_request_content_type
65
+ end
66
+
67
+ def determine_request_content_type
68
+ default_type = 'application/json;charset=utf-8'
69
+ return default_type unless @unverified.request
70
+ return default_type unless @unverified.request.options
71
+ return default_type unless @unverified.request.options[:headers]
72
+ @unverified.request.options[:headers]['Content-Type']
73
+ end
74
+
75
+ def parse_method(method)
76
+ case method.to_s
77
+ when Net::HTTP::Post.to_s
78
+ 'POST'
79
+ else
80
+ 'GET'
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trusona
4
+ ##
5
+ # Helpful for finding Devices to determine if and when they were activated
6
+ class Device
7
+ ##
8
+ # Finds the specified device
9
+ #
10
+ # @param [String] id The identifier of the Device
11
+ # @return [Trusona::Resources::Device] the Device
12
+ # @raise ArgumentError if the identifier is nil
13
+ #
14
+ # @raise [Trusona::InvalidResourceError] if the resource is not +valid?+
15
+ # @see Trusona::Resources::BaseResource#valid?
16
+ # @raise [Trusona::BadRequestError] if the request is improperly formatted
17
+ # @raise [Trusona::UnauthorizedRequestError] if the request is unauthorized.
18
+ # Typically the result of invalid or revoked Trusona SDK keys.
19
+ # @raise [Trusona::ApiError] if the Trusona API is experiencing problems.
20
+ #
21
+ # @example
22
+ #
23
+ # Trusona::Device.find(id: 'r1ByVyVKJ7TRgU0RPX0-THMTD_CO3VrCSNqLpJFmhms')
24
+ #
25
+ def self.find(id: nil)
26
+ raise ArgumentError, 'A Device identifier is required.' unless id
27
+
28
+ Trusona::Workers::DeviceFinder.new.find(id)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trusona
4
+ ##
5
+ # Manages the creation of device and user bindings
6
+ class DeviceUserBinding
7
+ ##
8
+ # Binds a device identifier and user identifier together in the Trusona API
9
+ #
10
+ # @param [String] user The user identifier that uniquely identifies the
11
+ # user in the Relying Party system.
12
+ # @param [String] device The device identifier as retrieved by the Trusona
13
+ # mobile SDK.
14
+ # @return [Trusona::Resources::DeviceUserBinding] The created device and
15
+ # user binding record.
16
+ #
17
+ # @see Trusona::Resources::DeviceUserBinding
18
+ #
19
+ # @raise [Trusona::InvalidResourceError] if the resource is not +valid?+
20
+ # @see Trusona::Resources::BaseResource#valid?
21
+ # @raise [Trusona::BadRequestError] if the request is improperly formatted
22
+ # @raise [Trusona::UnauthorizedRequestError] if the request is unauthorized.
23
+ # Typically the result of invalid or revoked Trusona SDK keys.
24
+ # @raise [Trusona::ApiError] if the Trusona API is experiencing problems.
25
+ #
26
+ # @example
27
+ #
28
+ # binding = Trusona::DeviceUserBinding.create(
29
+ # user: '83452353-4F7B-4CA2-BBCD-57ACE7279EA0',
30
+ # device: 'PBanKaajTmz_Cq1pDkrRzyeISBSBoGjExzp5r6-UjcI'
31
+ # )
32
+ #
33
+
34
+ def self.create(user: nil, device: nil)
35
+ raise ArgumentError, 'User is missing' if user.nil? || user.empty?
36
+ raise ArgumentError, 'Device is missing' if device.nil? || device.empty?
37
+ Trusona::Workers::DeviceUserBindingCreator.new.create(
38
+ user: user, device: device
39
+ )
40
+ end
41
+
42
+ ##
43
+ # Activates an existing device and user binding record
44
+ #
45
+ # @param id [String] The ID of the {Trusona::Resources::DeviceUserBinding}
46
+ # to be activated
47
+ # @raise (see .create)
48
+ # @return [Trusona::Resources::DeviceUserBindingActivation]
49
+ #
50
+
51
+ def self.activate(id: nil)
52
+ raise ArgumentError if id.nil? || id.empty?
53
+ Trusona::Workers::DeviceUserBindingActivator.new.activate(id: id)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trusona
4
+ ##
5
+ # A generic error for when things go wrong, but we're not sure why
6
+ class TrusonaError < StandardError; end
7
+
8
+ ##
9
+ # An error that occurs when the configuration is incorrect or invalid
10
+ class ConfigurationError < TrusonaError; end
11
+
12
+ ##
13
+ # An error resulting from a resource that is invalid due to missing
14
+ # required fields
15
+ class InvalidResourceError < TrusonaError; end
16
+
17
+ ##
18
+ # An error used to indicate a failure in HMAC signing
19
+ class SigningError < TrusonaError; end
20
+
21
+ ##
22
+ # An error reserved for problems communicating with the Trusona REST API
23
+ class RequestError < TrusonaError; end
24
+
25
+ ##
26
+ # The result of attempting to access the Trusona REST API with invalid
27
+ # or missing SDK Keys (Token and Secret)
28
+ class UnauthorizedRequestError < RequestError; end
29
+
30
+ ##
31
+ # An error for resulting from 50x errors from the Trusona REST API
32
+ class ApiError < RequestError; end
33
+
34
+ ##
35
+ # An error resulting from 404 errors from the Trusona REST API
36
+ class ResourceNotFoundError < RequestError; end
37
+
38
+ ##
39
+ # An error resulting from 400 errors from the Trusona REST API
40
+ class BadRequestError < RequestError; end
41
+
42
+ ##
43
+ # An error resulting from 424 errors from the Trusona REST API
44
+ class FailedDependencyError < RequestError; end
45
+
46
+ ##
47
+ # An error resulting from trying to find a record without a valid
48
+ # identifier. Prempts a `Trusona::ResourceNotFoundError` error by
49
+ # not making a network request with an invalid identifier.
50
+ class InvalidRecordIdentifier < TrusonaError; end
51
+ end