myinfo 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9c42852df0d1859a05e3b6626cc2abb19945cc2072d5cf2313bacc09ce15eb0b
4
+ data.tar.gz: 10b5b50b6882dad300511581299872d330df87b3d2df66734af3391406562ead
5
+ SHA512:
6
+ metadata.gz: dbee0afc819ced257f4392c0983c787e72347e9248cff74e3d1ff4885314005adc744fb4f14c04e5dff6e35da58aa80df7db74e440600636bc0b6cb727cfca80
7
+ data.tar.gz: be29694339f25994661aafa7d2c581ee94a0179d3b66c4db123ccc71a3fc8b51a925dd21a662c3fd9bc44f12d62fea052c736d7c3c1957ff9dab06bbae7cd952
@@ -0,0 +1,112 @@
1
+ # Rails wrapper for MyInfo API
2
+
3
+ ![tests](https://github.com/GovTechSG/myinfo/workflows/tests/badge.svg?branch=main)
4
+
5
+
6
+ [MyInfo Documentation (Public)](https://public.cloud.myinfo.gov.sg/myinfo/api/myinfo-kyc-v3.1.0.html)
7
+
8
+ [MyInfo Documentation (Government)](https://public.cloud.myinfo.gov.sg/myinfo/tuo/myinfo-tuo-specs.html)
9
+ ## Basic Setup (Public)
10
+
11
+ 1. `bundle add myinfo`
12
+ 2. Create a `config/initializers/myinfo.rb` and add the required configuration based on your environment.
13
+ ```ruby
14
+ MyInfo.configure do |config|
15
+ config.app_id = ''
16
+ config.client_id = ''
17
+ config.client_secret = ''
18
+ config.base_url = 'test.api.myinfo.gov.sg' # don't set https://
19
+ config.redirect_uri = 'https://localhost:3001/callback'
20
+ config.public_facing = true
21
+ config.private_key = File.read(Rails.root.join('private_key_location'))
22
+ config.public_cert = File.read(Rails.root.join('public_cert_location'))
23
+ config.sandbox = false # optional, false by default
24
+ config.proxy = { address: 'proxy_address', port: 'proxy_port' } # optional, nil by default
25
+ end
26
+ ```
27
+
28
+ 3. To obtain a person's MyInfo information, we need to authorise the query first:
29
+ ```ruby
30
+ redirect_to MyInfo::V3::AuthoriseUrl.call(
31
+ purpose: 'set your purpose here',
32
+ state: SecureRandom.hex # set a state to check on callback
33
+ )
34
+ ```
35
+
36
+ 4. On `redirect_url`, obtain a `MyInfo::V3::Token`. This token can only be used once.
37
+ ```ruby
38
+ response = MyInfo::V3::Token.call(
39
+ code: params[:code],
40
+ state: params[:state]
41
+ )
42
+ ```
43
+
44
+ 5. Obtain the `access_token` from the `response` and query for `MyInfo::V3::Person`:
45
+ ```ruby
46
+ result = MyInfo::V3::Person.call(access_token: response.data) if response.success?
47
+ ```
48
+
49
+ ## Basic Setup (Government)
50
+
51
+ 1. `bundle add myinfo`
52
+ 2. Create a `config/initializers/myinfo.rb` and add the required configuration based on your environment.
53
+ ```ruby
54
+ MyInfo.configure do |config|
55
+ config.app_id = ''
56
+ config.client_id = ''
57
+ config.client_secret = ''
58
+ config.base_url = 'test.api.myinfo.gov.sg' # don't set https://
59
+ config.redirect_uri = 'https://localhost:3001/callback'
60
+ config.singpass_eservice_id = 'MYINFO-CONSENTPLATFORM'
61
+ config.private_key = File.read(Rails.root.join('private_key_location'))
62
+ config.public_cert = File.read(Rails.root.join('public_cert_location'))
63
+ config.sandbox = false # optional, false by default
64
+ config.proxy = { address: 'proxy_address', port: 'proxy_port' } # optional, nil by default
65
+ end
66
+ ```
67
+
68
+ 3. To obtain a person's MyInfo information, we need to authorise the query first:
69
+ ```ruby
70
+ redirect_to MyInfo::V3::AuthoriseUrl.call(
71
+ nric_fin: "user's NRIC", # see documentation for list of sample NRICs
72
+ purpose: 'set your purpose here',
73
+ state: SecureRandom.hex # set a state to check on callback
74
+ )
75
+ ```
76
+
77
+ 4. On `redirect_url`, obtain a `MyInfo::V3::Token`. This token can only be used once.
78
+ ```ruby
79
+ response = MyInfo::V3::Token.call(
80
+ code: params[:code],
81
+ state: params[:state]
82
+ )
83
+ ```
84
+
85
+ 5. Obtain the `access_token` from the `response` and query for `MyInfo::V3::Person`:
86
+ ```ruby
87
+ result = MyInfo::V3::Person.call(access_token: response.data) if response.success?
88
+ ```
89
+
90
+ ## Sample App Demo
91
+
92
+ 1. `git clone git@github.com:GovTechSG/myinfo-rails.git`
93
+ 2. `cd myinfo-rails`
94
+ 3. `bundle install`
95
+ 4. `cd spec/dummy && rails s`
96
+ 5. Navigate to `localhost:3001`
97
+
98
+ ## Advanced
99
+ - `attributes` can be passed to `AuthoriseUrl` and `Person` as an array to override the default attributes queried - check MyInfo for a list of available attributes.
100
+
101
+ - `success?` can be called on `MyInfo::V3::Response` to determine whether the query has succeeded or failed. Check MyInfo API for a list of responses and how to handle them.
102
+
103
+ ## Disclaimer
104
+ Provided credentials in the repository are either obtained from [MyInfo Demo App](https://github.com/ndi-trusted-data/myinfo-demo-app) or samples online, and are only for testing purposes. They should not be re-used for staging or production environments. Visit the [official MyInfo tutorial](https://www.ndi-api.gov.sg/library/myinfo/tutorial3) for more information.
105
+
106
+ ## Contributing
107
+
108
+ Contributions are welcome!
109
+
110
+ 1. Fork the repository
111
+ 2. Write code and tests
112
+ 3. Submit a PR
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ require_relative 'myinfo/errors'
8
+
9
+ require_relative 'myinfo/helpers/callable'
10
+ require_relative 'myinfo/helpers/attributes'
11
+
12
+ require_relative 'myinfo/v3/response'
13
+ require_relative 'myinfo/v3/api'
14
+ require_relative 'myinfo/v3/token'
15
+ require_relative 'myinfo/v3/person'
16
+ require_relative 'myinfo/v3/person_basic'
17
+ require_relative 'myinfo/v3/authorise_url'
18
+
19
+ # Base MyInfo class
20
+ module MyInfo
21
+ class << self
22
+ attr_accessor :configuration
23
+ end
24
+
25
+ def self.configure
26
+ self.configuration ||= Configuration.new
27
+ yield(configuration)
28
+ end
29
+
30
+ # Configuration to set various properties needed to use MyInfo
31
+ class Configuration
32
+ attr_accessor :singpass_eservice_id, :app_id, :base_url, :client_id, :proxy,
33
+ :private_key, :public_cert, :client_secret, :redirect_uri
34
+
35
+ attr_writer :public_facing, :sandbox
36
+
37
+ def initialize
38
+ @public_facing = false
39
+ @sandbox = false
40
+ @proxy = { address: nil, port: nil }
41
+ end
42
+
43
+ def base_url_with_protocol
44
+ "https://#{base_url}"
45
+ end
46
+
47
+ def public?
48
+ @public_facing
49
+ end
50
+
51
+ def sandbox?
52
+ @sandbox
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyInfo
4
+ class MissingConfigurationError < StandardError; end
5
+
6
+ class UnavailableError < StandardError; end
7
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyInfo
4
+ # Attributes parsing
5
+ module Attributes
6
+ DEFAULT_VALUES = %i[name sex race dob residentialstatus email mobileno regadd].freeze
7
+
8
+ def self.parse(attributes)
9
+ attributes ||= DEFAULT_VALUES
10
+
11
+ attributes.is_a?(String) ? attributes : attributes.join(',')
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyInfo
4
+ # Service Object
5
+ module Callable
6
+ def call(**kwargs)
7
+ new(**kwargs).call
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwe'
4
+ require 'jwt'
5
+
6
+ module MyInfo
7
+ module V3
8
+ # Base API class
9
+ class Api
10
+ extend Callable
11
+
12
+ def endpoint
13
+ raise NotImplementedError, 'abstract'
14
+ end
15
+
16
+ def params(_args)
17
+ raise NotImplementedError, 'abstract'
18
+ end
19
+
20
+ def http_method
21
+ 'GET'
22
+ end
23
+
24
+ def support_gzip?
25
+ false
26
+ end
27
+
28
+ def header(params:, access_token: nil)
29
+ {
30
+ 'Content-Type' => 'application/json',
31
+ 'Accept' => 'application/json',
32
+ 'Cache-Control' => 'no-cache'
33
+ }.tap do |values|
34
+ values['Authorization'] = auth_header(params: params, access_token: access_token) unless config.sandbox?
35
+
36
+ if support_gzip?
37
+ values['Accept-Encoding'] = 'gzip'
38
+ values['Content-Encoding'] = 'gzip'
39
+ end
40
+ end
41
+ end
42
+
43
+ def parse_response(response)
44
+ if response.code == '200'
45
+ yield
46
+ elsif errors.include?(response.code)
47
+ json = JSON.parse(response.body)
48
+
49
+ Response.new(success: false, data: "#{json['code']} - #{json['message']}")
50
+ else
51
+ Response.new(success: false, data: "#{response.code} - #{response.body}")
52
+ end
53
+ end
54
+
55
+ protected
56
+
57
+ def decrypt_jwe(text)
58
+ if config.sandbox?
59
+ JSON.parse(text)
60
+ else
61
+ JWE.decrypt(text, private_key)
62
+ end
63
+ end
64
+
65
+ def decode_jws(jws)
66
+ # TODO: verify signature
67
+ JWT.decode(jws, public_key, true, algorithm: 'RS256').first
68
+ end
69
+
70
+ def http
71
+ @http ||= if config.proxy.blank?
72
+ Net::HTTP.new(config.base_url, 443)
73
+ else
74
+ Net::HTTP.new(config.base_url, 443, config.proxy[:address], config.proxy[:port])
75
+ end
76
+
77
+ @http.use_ssl = true
78
+ @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
79
+
80
+ @http
81
+ end
82
+
83
+ def config
84
+ MyInfo.configuration
85
+ end
86
+
87
+ private
88
+
89
+ def private_key
90
+ raise MissingConfigurationError, :private_key if config.private_key.blank?
91
+
92
+ OpenSSL::PKey::RSA.new(config.private_key)
93
+ end
94
+
95
+ def public_key
96
+ raise MissingConfigurationError, :public_cert if config.public_cert.blank?
97
+
98
+ OpenSSL::X509::Certificate.new(config.public_cert).public_key
99
+ end
100
+
101
+ def to_query(headers)
102
+ headers.sort_by { |k, v| [k.to_s, v] }
103
+ .map { |arr| arr.join('=') }
104
+ .join('&')
105
+ end
106
+
107
+ def auth_header(params:, access_token: nil)
108
+ auth_headers = {
109
+ app_id: config.app_id,
110
+ nonce: SecureRandom.hex,
111
+ signature_method: 'RS256',
112
+ timestamp: (Time.now.to_f * 1000).to_i
113
+ }.merge(params)
114
+
115
+ auth_headers[:signature] = sign(auth_headers)
116
+
117
+ header_elements = auth_headers.map { |k, v| "#{k}=\"#{v}\"" }
118
+ header_elements << "Bearer #{access_token}" if access_token.present?
119
+
120
+ "PKI_SIGN #{header_elements.join(',')}"
121
+ end
122
+
123
+ def sign(headers)
124
+ headers_query = to_query(headers)
125
+ base_string = "#{http_method}&#{config.base_url_with_protocol}/#{slug}&#{headers_query}"
126
+ signed_string = private_key.sign(OpenSSL::Digest.new('SHA256'), base_string)
127
+ Base64.strict_encode64(signed_string)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyInfo
4
+ module V3
5
+ # https://public.cloud.myinfo.gov.sg/myinfo/tuo/myinfo-tuo-specs.html#operation/getauthorise
6
+ class AuthoriseUrl
7
+ extend Callable
8
+
9
+ attr_accessor :nric_fin, :attributes, :purpose, :state, :authmode, :login_type
10
+
11
+ def initialize(purpose:, state:, nric_fin: nil, authmode: 'SINGPASS', login_type: 'SINGPASS', attributes: nil)
12
+ @nric_fin = nric_fin
13
+ @attributes = Attributes.parse(attributes)
14
+ @purpose = purpose
15
+ @authmode = authmode
16
+ @login_type = login_type
17
+ @state = state
18
+ end
19
+
20
+ def call
21
+ query_string = {
22
+ authmode: authmode,
23
+ login_type: login_type,
24
+ purpose: purpose,
25
+ client_id: config.client_id,
26
+ attributes: attributes,
27
+ sp_esvcId: config.singpass_eservice_id,
28
+ state: state,
29
+ redirect_uri: config.redirect_uri
30
+ }.compact.to_param
31
+
32
+ endpoint(query_string)
33
+ end
34
+
35
+ def endpoint(query_string)
36
+ if config.public?
37
+ "#{config.base_url_with_protocol}/#{slug}/?#{query_string}"
38
+ else
39
+ "#{config.base_url_with_protocol}/#{slug}/#{nric_fin}/?#{query_string}"
40
+ end
41
+ end
42
+
43
+ def slug
44
+ slug_prefix = config.public? ? 'com' : 'gov'
45
+
46
+ "#{slug_prefix}/v3/authorise"
47
+ end
48
+
49
+ def config
50
+ MyInfo.configuration
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyInfo
4
+ module V3
5
+ # Calls the Person API
6
+ class Person < Api
7
+ attr_accessor :access_token, :decoded_token, :attributes, :txn_no
8
+
9
+ def initialize(access_token:, txn_no: nil, attributes: nil)
10
+ @access_token = access_token
11
+ @decoded_token = decode_jws(access_token)
12
+ @attributes = Attributes.parse(attributes)
13
+ @txn_no = txn_no
14
+ end
15
+
16
+ def call
17
+ headers = header(params: params, access_token: access_token)
18
+ endpoint_url = "/#{slug}?#{params.to_query}"
19
+
20
+ response = http.request_get(endpoint_url, headers)
21
+ parse_response(response)
22
+ end
23
+
24
+ def slug
25
+ slug_prefix = config.public? ? 'com' : 'gov'
26
+
27
+ "#{slug_prefix}/v3/person/#{nric_fin}/"
28
+ end
29
+
30
+ def support_gzip?
31
+ true
32
+ end
33
+
34
+ def params
35
+ {
36
+ txnNo: txn_no,
37
+ attributes: attributes,
38
+ client_id: config.client_id,
39
+ sp_esvcId: config.singpass_eservice_id
40
+ }.compact
41
+ end
42
+
43
+ def nric_fin
44
+ @nric_fin ||= decoded_token['sub']
45
+ end
46
+
47
+ def errors
48
+ %w[401 403 404]
49
+ end
50
+
51
+ def parse_response(response)
52
+ super do
53
+ json = decrypt_jwe(response.body)
54
+ json = decode_jws(json.delete('"')) unless config.sandbox?
55
+
56
+ Response.new(success: true, data: json)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyInfo
4
+ module V3
5
+ # Calls the PersonBasic API
6
+ class PersonBasic < Api
7
+ attr_accessor :nric_fin, :attributes, :txn_no
8
+
9
+ def initialize(nric_fin:, txn_no: nil, attributes: nil)
10
+ raise UnavailableError, 'person-basic endpoint is not available for public-facing APIs.' if config.public?
11
+
12
+ @attributes = Attributes.parse(attributes)
13
+ @nric_fin = nric_fin
14
+ @txn_no = txn_no
15
+ end
16
+
17
+ def call
18
+ headers = header(params: params)
19
+ endpoint_url = "/#{slug}?#{params.to_query}"
20
+
21
+ response = http.request_get(endpoint_url, headers)
22
+ parse_response(response)
23
+ end
24
+
25
+ def slug
26
+ "gov/v3/person-basic/#{nric_fin}/"
27
+ end
28
+
29
+ def support_gzip?
30
+ true
31
+ end
32
+
33
+ def params
34
+ {
35
+ txnNo: txn_no,
36
+ attributes: attributes,
37
+ client_id: config.client_id,
38
+ sp_esvcId: config.singpass_eservice_id
39
+ }.compact
40
+ end
41
+
42
+ def errors
43
+ %w[401 403 404 428 default]
44
+ end
45
+
46
+ def parse_response(response)
47
+ super do
48
+ json = decrypt_jwe(response.body)
49
+ json = decode_jws(json.delete('\"')) unless config.sandbox?
50
+
51
+ Response.new(success: true, data: json)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyInfo
4
+ module V3
5
+ # Simple response wrapper
6
+ class Response
7
+ attr_accessor :success, :data
8
+
9
+ def initialize(success:, data:)
10
+ @success = success
11
+ @data = data
12
+ end
13
+
14
+ def success?
15
+ @success
16
+ end
17
+
18
+ def to_s
19
+ data
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyInfo
4
+ module V3
5
+ # Called after authorise to obtain a token for API calls
6
+ class Token < Api
7
+ attr_accessor :code, :state
8
+
9
+ def initialize(code:, state: nil)
10
+ @code = code
11
+ @state = state
12
+ end
13
+
14
+ def call
15
+ headers = header(params: params).merge({ 'Content-Type' => 'application/x-www-form-urlencoded' })
16
+ response = http.request_post("/#{slug}", params.to_param, headers)
17
+
18
+ parse_response(response)
19
+ end
20
+
21
+ def http_method
22
+ 'POST'
23
+ end
24
+
25
+ def slug
26
+ slug_prefix = config.public? ? 'com' : 'gov'
27
+
28
+ "#{slug_prefix}/v3/token"
29
+ end
30
+
31
+ def params
32
+ {
33
+ code: code,
34
+ state: state,
35
+ client_id: config.client_id,
36
+ client_secret: config.client_secret,
37
+ grant_type: 'authorization_code',
38
+ redirect_uri: config.redirect_uri
39
+ }.compact
40
+ end
41
+
42
+ def errors
43
+ %w[400 401]
44
+ end
45
+
46
+ def parse_response(response)
47
+ super do
48
+ json = JSON.parse(response.body)
49
+ access_token = json['access_token']
50
+
51
+ Response.new(success: true, data: access_token)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
metadata ADDED
@@ -0,0 +1,179 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: myinfo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Lim Yao Jie
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-01-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwe
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '6.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '6.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.10'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.10'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '4.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '4.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.8'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.8'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.21'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.21'
125
+ - !ruby/object:Gem::Dependency
126
+ name: webmock
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.11'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.11'
139
+ description: Rails wrapper for MyInfo API
140
+ email: limyaojie93@gmail.com
141
+ executables: []
142
+ extensions: []
143
+ extra_rdoc_files: []
144
+ files:
145
+ - README.md
146
+ - lib/myinfo.rb
147
+ - lib/myinfo/errors.rb
148
+ - lib/myinfo/helpers/attributes.rb
149
+ - lib/myinfo/helpers/callable.rb
150
+ - lib/myinfo/v3/api.rb
151
+ - lib/myinfo/v3/authorise_url.rb
152
+ - lib/myinfo/v3/person.rb
153
+ - lib/myinfo/v3/person_basic.rb
154
+ - lib/myinfo/v3/response.rb
155
+ - lib/myinfo/v3/token.rb
156
+ homepage: https://rubygems.org/gems/myinfo
157
+ licenses:
158
+ - MIT
159
+ metadata: {}
160
+ post_install_message:
161
+ rdoc_options: []
162
+ require_paths:
163
+ - lib
164
+ required_ruby_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - "~>"
167
+ - !ruby/object:Gem::Version
168
+ version: '2.7'
169
+ required_rubygems_version: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ requirements: []
175
+ rubygems_version: 3.1.4
176
+ signing_key:
177
+ specification_version: 4
178
+ summary: MyInfo gem
179
+ test_files: []