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.
- checksums.yaml +7 -0
- data/README.md +112 -0
- data/lib/myinfo.rb +55 -0
- data/lib/myinfo/errors.rb +7 -0
- data/lib/myinfo/helpers/attributes.rb +14 -0
- data/lib/myinfo/helpers/callable.rb +10 -0
- data/lib/myinfo/v3/api.rb +131 -0
- data/lib/myinfo/v3/authorise_url.rb +54 -0
- data/lib/myinfo/v3/person.rb +61 -0
- data/lib/myinfo/v3/person_basic.rb +56 -0
- data/lib/myinfo/v3/response.rb +23 -0
- data/lib/myinfo/v3/token.rb +56 -0
- metadata +179 -0
    
        checksums.yaml
    ADDED
    
    | @@ -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
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,112 @@ | |
| 1 | 
            +
            # Rails wrapper for MyInfo API
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            
         | 
| 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
         | 
    
        data/lib/myinfo.rb
    ADDED
    
    | @@ -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,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,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: []
         |