myinfo 0.1.0

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