Skip to content

Commit

Permalink
Network API: SIM Swap and Number Verification implementation (#313)
Browse files Browse the repository at this point in the history
Implementing Network SIM Swap and Number Verification APIs
  • Loading branch information
superchilled authored Aug 8, 2024
1 parent a488667 commit 9327396
Show file tree
Hide file tree
Showing 23 changed files with 830 additions and 19 deletions.
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 7.26.0

* Implements the Network Number Verification and Network SIM Swap APIs. [#313](https://github.com/Vonage/vonage-ruby-sdk/pull/313)
* Makes some minor updates to the Voice and Verify2 implementations. [#312](https://github.com/Vonage/vonage-ruby-sdk/pull/312)

# 7.25.0

* Validation updates to Verify v2 SMS and WhatsApp channels. [#309](https://github.com/Vonage/vonage-ruby-sdk/pull/309)
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ gem 'sorbet', :group => :development
gem 'sorbet-runtime'
gem 'phonelib'
gem 'codecov', :require => false, :group => :test
group :test, :development do
gem 'pry'
end
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,8 @@ The following is a list of Vonage APIs for which the Ruby SDK currently provides
* [Conversation API](https://developer.vonage.com/en/conversation/overview)
* [Meetings API](https://developer.vonage.com/en/meetings/overview)
* [Messages API](https://developer.vonage.com/en/messages/overview)
* [Network Number Verification API](https://developer.vonage.com/en/number-verification/overview)
* [Network SIM Swap API](https://developer.vonage.com/en/sim-swap/overview)
* [Number Insight API](https://developer.vonage.com/en/number-insight/overview)
* [Numbers API](https://developer.vonage.com/en/numbers/overview)
* [Proactive Connect API](https://developer.vonage.com/en/proactive-connect/overview) *
Expand Down
1 change: 1 addition & 0 deletions lib/vonage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module Vonage
'jwt' => 'JWT',
'sip' => 'SIP',
'sms' => 'SMS',
'network_sim_swap' => 'NetworkSIMSwap',
'mms' => 'MMS',
'tfa' => 'TFA',
'version' => 'VERSION',
Expand Down
2 changes: 1 addition & 1 deletion lib/vonage/basic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module Vonage
class Basic < AbstractAuthentication
def update(object)
def update(object, data)
return unless object.is_a?(Net::HTTPRequest)

object.basic_auth(@config.api_key, @config.api_secret)
Expand Down
2 changes: 1 addition & 1 deletion lib/vonage/bearer_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

module Vonage
class BearerToken < AbstractAuthentication
def update(object)
def update(object, data)
return unless object.is_a?(Net::HTTPRequest)

object['Authorization'] = 'Bearer ' + @config.token
Expand Down
14 changes: 14 additions & 0 deletions lib/vonage/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ def messaging
@messaging ||= T.let(Messaging.new(config), T.nilable(Vonage::Messaging))
end

# @return [NetworkNumberVerification]
#
sig { returns(T.nilable(Vonage::NetworkNumberVerification)) }
def network_number_verification
@network_number_verification ||= T.let(NetworkNumberVerification.new(config), T.nilable(Vonage::NetworkNumberVerification))
end

# @return [NetworkSIMSwap]
#
sig { returns(T.nilable(Vonage::NetworkSIMSwap)) }
def network_sim_swap
@network_sim_swap ||= T.let(NetworkSIMSwap.new(config), T.nilable(Vonage::NetworkSIMSwap))
end

# @return [NumberInsight]
#
sig { returns(T.nilable(Vonage::NumberInsight)) }
Expand Down
5 changes: 3 additions & 2 deletions lib/vonage/key_secret_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ class KeySecretParams < AbstractAuthentication
extend T::Sig

sig { params(
object: T.any(T::Hash[T.untyped, T.untyped], URI::HTTPS, Net::HTTP::Post, Net::HTTP::Get)
object: T.any(T::Hash[T.untyped, T.untyped], URI::HTTPS, Net::HTTP::Post, Net::HTTP::Get),
data: T.nilable(Hash)
).void }
def update(object)
def update(object, data)
return unless object.is_a?(Hash)

@config = T.let(@config, T.nilable(Vonage::Config))
Expand Down
5 changes: 4 additions & 1 deletion lib/vonage/keys.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ def camelcase(hash)
'max_duration',
'partial_captions',
'status_callback_url',
'audio_rate'
'audio_rate',
'phone_number',
'hashed_phone_number',
'max_age'
]
hash.transform_keys do |k|
if exceptions.include?(k.to_s)
Expand Down
21 changes: 9 additions & 12 deletions lib/vonage/namespace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,15 @@ def self.request_headers
Post = Net::HTTP::Post
Delete = Net::HTTP::Delete

def build_request(path:, type: Get, params: {})
def build_request(path:, type: Get, params: {}, auth_data: nil)
authentication = self.class.authentication.new(@config)
authentication.update(params)
authentication.update(params, auth_data)

uri = URI("https://" + @host + path)
unless type.const_get(:REQUEST_HAS_BODY) || params.empty?
uri.query = Params.encode(params)
end

# Set BasicAuth if neeeded
authentication.update(uri)

# instantiate request
request = type.new(uri)

Expand All @@ -80,8 +77,8 @@ def build_request(path:, type: Get, params: {})
request["Accept"] = "application/json"
self.class.request_headers.each { |key, value| request[key] = value }

# Set BearerToken if needed
authentication.update(request)
# Set Authorization header if needed
authentication.update(request, auth_data)

# set body
if type.const_get(:REQUEST_HAS_BODY)
Expand All @@ -106,7 +103,7 @@ def make_request!(request, &block)
response
end

def request(path, params: nil, type: Get, response_class: Response, &block)
def request(path, params: nil, type: Get, response_class: Response, auth_data: nil, &block)
auto_advance =
(
if !params.nil? && params.key?(:auto_advance)
Expand All @@ -120,7 +117,7 @@ def request(path, params: nil, type: Get, response_class: Response, &block)
params.tap { |params| params.delete(:auto_advance) } if !params.nil? &&
params.key?(:auto_advance)

request = build_request(path: path, params: params || {}, type: type)
request = build_request(path: path, params: params || {}, type: type, auth_data: auth_data)

response = make_request!(request, &block)

Expand All @@ -139,7 +136,7 @@ def request(path, params: nil, type: Get, response_class: Response, &block)
end
end

def multipart_post_request(path, filepath:, file_name:, mime_type:, params: {}, override_uri: nil, no_auth: false, response_class: Response, &block)
def multipart_post_request(path, filepath:, file_name:, mime_type:, params: {}, override_uri: nil, no_auth: false, response_class: Response, auth_data: nil, &block)
authentication = self.class.authentication.new(@config) unless no_auth

uri = override_uri ? URI(override_uri) : URI('https://' + @host + path)
Expand All @@ -156,8 +153,8 @@ def multipart_post_request(path, filepath:, file_name:, mime_type:, params: {},

request['User-Agent'] = UserAgent.string(@config.app_name, @config.app_version)

# Set BearerToken if needed
authentication.update(request) unless no_auth
# Set Authorization header if needed
authentication.update(request, auth_data) unless no_auth

logger.log_request_info(request)

Expand Down
22 changes: 22 additions & 0 deletions lib/vonage/network_authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# typed: true
# frozen_string_literal: true

module Vonage
class NetworkAuthentication < AbstractAuthentication
def update(object, data)
return unless object.is_a?(Net::HTTPRequest)

token = self.public_send(data[:auth_flow]).token(**data)

object['Authorization'] = 'Bearer ' + token
end

def client_authentication
@client_authentication ||= ClientAuthentication.new(@config)
end

def server_authentication
@server_authentication ||= ServerAuthentication.new(@config)
end
end
end
39 changes: 39 additions & 0 deletions lib/vonage/network_authentication/client_authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# typed: true
# frozen_string_literal: true

module Vonage
class NetworkAuthentication::ClientAuthentication < Namespace
extend T::Sig

self.authentication = BearerToken

self.host = :vonage_host

self.request_headers['Content-Type'] = 'application/x-www-form-urlencoded'

def token(oidc_auth_code:, redirect_uri:, **params)
request(
'/oauth2/token',
params: {
grant_type: 'authorization_code',
code: oidc_auth_code,
redirect_uri: redirect_uri
},
type: Post
).access_token
end

def generate_oidc_uri(purpose:, api_scope:, login_hint:, redirect_uri:, state:)
scope = "openid%20dpv:#{purpose}%23#{api_scope}"
uri = "https://oidc.idp.vonage.com/oauth2/auth?" +
"client_id=#{@config.application_id}" +
"&response_type=code" +
"&scope=#{scope}" +
"&login_hint=#{login_hint}" +
"&redirect_uri=#{redirect_uri}" +
"&state=#{state}"

uri
end
end
end
47 changes: 47 additions & 0 deletions lib/vonage/network_authentication/server_authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# typed: true
# frozen_string_literal: true

module Vonage
class NetworkAuthentication::ServerAuthentication < Namespace
extend T::Sig

self.authentication = BearerToken

self.host = :vonage_host

self.request_headers['Content-Type'] = 'application/x-www-form-urlencoded'

def token(purpose:, api_scope:, login_hint:, **params)
auth_req_id = bc_authorize(
purpose: purpose,
api_scope: api_scope,
login_hint: login_hint
).auth_req_id

request_access_token(auth_req_id: auth_req_id).access_token
end

def bc_authorize(purpose:, api_scope:, login_hint:)
scope = "openid dpv:#{purpose}##{api_scope}"
request(
"/oauth2/bc-authorize",
params: {
scope: scope,
login_hint: login_hint
},
type: Post
)
end

def request_access_token(auth_req_id:)
request(
"/oauth2/token",
params: {
grant_type: 'urn:openid:params:grant-type:ciba',
auth_req_id: auth_req_id
},
type: Post
)
end
end
end
92 changes: 92 additions & 0 deletions lib/vonage/network_number_verification.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# typed: strict
# frozen_string_literal: true
require 'phonelib'

module Vonage
class NetworkNumberVerification < Namespace
extend T::Sig
include Keys

self.authentication = NetworkAuthentication

self.host = :vonage_host

self.request_body = JSON

# Verifies if the specified phone number (plain text or hashed format) matches the one that the user is currently using.
#
# @example
# response = client.network_number_verification.verify(
# phone_number: '+447900000000',
# auth_data: {
# oidc_auth_code: '0dadaeb4-7c79-4d39-b4b0-5a6cc08bf537',
# redirect_uri: 'https://example.com/callback'
# }
# )
#
# @param [required, String] :phone_number The phone number to check, in the E.164 format, prepended with a `+`.
#
# @param [required, Hash] :auth_data A hash of authentication data required for the client token request. Must contain the following keys:
# @option auth_data [required, String] :oidc_auth_code The OIDC auth code.
# @option auth_data [required, String] :redirect_uri The redirect URI.
# @see https://developer.vonage.com/en/getting-started-network/authentication#client-authentication-flow
#
# @return [Response]
#
# @see https://developer.vonage.com/en/api/camara/number-verification#verifyNumberVerification
#
sig { params(phone_number: String, auth_data: Hash).returns(Vonage::Response) }
def verify(phone_number:, auth_data:)
raise ArgumentError.new("`phone_number` must be in E.164 format") unless Phonelib.parse(phone_number).valid?
raise ArgumentError.new("`phone_number` must be prepended with a `+`") unless phone_number.start_with?('+')
raise ArgumentError.new("`auth_data` must contain key `:oidc_auth_code`") unless auth_data.has_key?(:oidc_auth_code)
raise ArgumentError.new("`auth_data[:oidc_auth_code]` must be a String") unless auth_data[:oidc_auth_code].is_a?(String)
raise ArgumentError.new("`auth_data` must contain key `:redirect_uri`") unless auth_data.has_key?(:redirect_uri)
raise ArgumentError.new("`auth_data[:redirect_uri]` must be a String") unless auth_data[:redirect_uri].is_a?(String)

params = {phone_number: phone_number}

request(
'/camara/number-verification/v031/verify',
params: camelcase(params),
type: Post,
auth_data: {
oidc_auth_code: auth_data[:oidc_auth_code],
redirect_uri: auth_data[:redirect_uri],
auth_flow: :client_authentication
}
)
end

# Creates a URL for a client-side OIDC request.
#
# @example
# response = client.network_number_verification.generate_oidc_uri(
# phone_number: '+447900000000',
# redirect_uri: 'https://example.com/callback'
# )
#
# @param [required, String] :phone_number The phone number that will be checked during the verification request.
#
# @param [required, String] :redirect_uri The URI that will receive the callback containing the OIDC auth code.
#
# @param [required, String] :state A string that you can use for tracking.
# Used to set a unique identifier for each access token you generate.
#
# @return [String]
#
# @see https://developer.vonage.com/en/getting-started-network/authentication#1-make-an-oidc-request
sig { params(phone_number: String, redirect_uri: String, state: String).returns(String) }
def generate_oidc_uri(phone_number:, redirect_uri:, state:)
params = {
purpose: 'FraudPreventionAndDetection',
api_scope: 'number-verification-verify-read',
login_hint: phone_number,
redirect_uri: redirect_uri,
state: state
}

Vonage::NetworkAuthentication::ClientAuthentication.new(@config).generate_oidc_uri(**params)
end
end
end
Loading

0 comments on commit 9327396

Please sign in to comment.