Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Otp voice support #47

Merged
merged 5 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ For rate limiting information, please confer to the [API Rate Limits](#api-rate-

### OTP Authentication

Send a user a one-time password (OTP) using your preferred delivery method (email/SMS). An email address or phone number must be provided accordingly.
Send a user a one-time password (OTP) using your preferred delivery method (email/SMS/Voice call). An email address or phone number must be provided accordingly.

The user can either `sign up`, `sign in` or `sign up or in`

Expand Down
45 changes: 30 additions & 15 deletions examples/ruby/otp_app.rb
Original file line number Diff line number Diff line change
@@ -1,60 +1,75 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative './version_check'
require 'descope'

@logger = Logger.new($stdout)

@project_id = ENV['DESCOPE_PROJECT_ID']
@management_key = ENV['DESCOPE_MANAGEMENT_KEY']

@logger.info("Initializing Descope API with project_id: #{@project_id} and base_uri: #{@base_uri}")

@client = Descope::Client.new({ project_id: @project_id, management_key: @management_key })
@logger.info("Initializing Descope API with project_id: #{@project_id} and base_uri: #{@client.base_uri}")

begin
puts "Please select OTP method (email, sms, whatsapp, voice):\n"
method = gets.chomp
@logger.info('Going to signup or in using OTP...')

@logger.info("Going to signup or in using OTP using #{method}...")
puts "Please select OTP method: [email, sms, voice]:\n"
method = gets.chomp

case method
when 'email'
requested_method = Descope::Mixins::Common::DeliveryMethod::EMAIL
puts "Please insert the email address you want to use:\n"
email = gets.chomp
requested_params = { login_id: email }
when 'sms'
requested_method = Descope::Mixins::Common::DeliveryMethod::SMS
when 'whatsapp'
requested_method = Descope::Mixins::Common::DeliveryMethod::WHATSAPP
@logger.info('Once signed up, we will use the update phone number')
puts "Please insert the phone number you want to use:\n"
phone = gets.chomp
requested_params = { login_id: phone }
when 'voice'
requested_method = Descope::Mixins::Common::DeliveryMethod::VOICE
@logger.info('Once signed up, we will use the update phone number')
puts "Please insert the phone number you want to use:\n"
phone = gets.chomp
requested_params = { login_id: phone }
else
raise 'Invalid method'
end
masked = @client.otp_sign_up_or_in(method: requested_method, login_id: email)

puts "Please insert the code you received by #{method} to #{masked}:\n"
@logger.info("Signing up using OTP with #{method}...")
if method == 'email'
user = { login_id: email, name: 'John Doe', email: email, phone: phone }
login_id = email
masked_method = @client.otp_sign_up(method: requested_method, user: user, login_id: email, phone: phone)
else
login_id = phone
masked_method = @client.otp_sign_up_or_in(method: requested_method, login_id: phone)
end

puts "Please insert the code you received by #{method} to #{masked_method}:\n"
value = gets.chomp

jwt_response = @client.otp_verify_code(method: requested_method, login_id: email, code: value)
jwt_response = @client.otp_verify_code(method: requested_method, login_id: login_id, code: value)
@logger.info('Code is valid')
puts "jwt_response: #{jwt_response}"
session_token = jwt_response[Descope::Mixins::Common::SESSION_TOKEN_NAME].fetch('jwt')
refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME].fetch('jwt')
@logger.info("jwt_response: #{jwt_response}")

@logger.info('going to validate session..')
@client.validate_session(session_token: session_token)
@client.validate_session(session_token:)
@logger.info('Session is valid and all is OK')

@logger.info('refreshing the session token..')
claims = @client.refresh_session(refresh_token: refresh_token)
claims = @client.refresh_session(refresh_token:)
@logger.info(
'going to revalidate the session with the newly refreshed token..'
)

new_session_token = claims.fetch(Descope::Mixins::Common::SESSION_TOKEN_NAME).fetch('jwt')
@client.validate_and_refresh_session(session_token: new_session_token, refresh_token: refresh_token)
@client.validate_and_refresh_session(session_token: new_session_token, refresh_token:)
@logger.info('Session is valid also for the refreshed token.')
rescue Descope::AuthException => e
@logger.error("Error: #{e.message}")
Expand Down
37 changes: 17 additions & 20 deletions lib/descope/api/v1/auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def exchange_access_key(access_key: nil, login_options: {}, audience: nil)
# Return value (Hash): returns the session token from the server together with the expiry and key id
# (sessionToken:Hash, keyId:str, expiration:int)
unless (access_key.is_a?(String) || access_key.nil?) && !access_key.to_s.empty?
raise Descope::AuthException, 'Access key should be a string!'
raise AuthException.new('Access key should be a string!', code: 400)
end

res = post(EXCHANGE_AUTH_ACCESS_KEY_PATH, { loginOptions: login_options, audience: }, {}, access_key)
Expand Down Expand Up @@ -407,6 +407,7 @@ def get_login_id_by_method(method: nil, user: {})
login_id = {
DeliveryMethod::WHATSAPP => ['whatsapp', user.fetch(:phone, '')],
DeliveryMethod::SMS => ['phone', user.fetch(:phone, '')],
DeliveryMethod::VOICE => ['phone', user.fetch(:phone, '')],
DeliveryMethod::EMAIL => ['email', user.fetch(:email, '')]
}[method]

Expand All @@ -416,34 +417,30 @@ def get_login_id_by_method(method: nil, user: {})
end

def adjust_and_verify_delivery_method(method, login_id, user)
return false if login_id.nil?
@logger.debug("adjust_and_verify_delivery_method: method: #{method}, login_id: #{login_id}, user: #{user}")
raise AuthException.new("Could not verify delivery method for method: #{method}", code: 400) if method.nil?
raise AuthException.new('Could not verify delivery method without login_id', code: 400) if login_id.nil?

return false unless user.is_a?(Hash)
unless user.is_a?(Hash)
raise AuthException.new('Could not verify delivery method, user is not a Hash', code: 400)
end

case method
when DeliveryMethod::EMAIL
user[:email] ||= login_id
begin
validate_email(user[:email])
return true
rescue AuthException
return false
end
when DeliveryMethod::SMS
user[:phone] ||= login_id
return false unless /^#{PHONE_REGEX}$/.match(user[:phone])
when DeliveryMethod::WHATSAPP
user[:phone] ||= login_id
return false unless /^#{PHONE_REGEX}$/.match(user[:phone])
validate_email(login_id)
@logger.debug("email: #{login_id} is valid")
true
when DeliveryMethod::SMS, DeliveryMethod::WHATSAPP, DeliveryMethod::VOICE
validate_phone(method, login_id)
@logger.debug("phone number (login_id): #{login_id} is valid")
true
else
return false
false
end

true
end

def extract_masked_address(response, method)
if [DeliveryMethod::SMS, DeliveryMethod::WHATSAPP].include?(method)
if [DeliveryMethod::SMS, DeliveryMethod::WHATSAPP, DeliveryMethod::VOICE].include?(method)
response['maskedPhone']
elsif method == DeliveryMethod::EMAIL
response['maskedEmail']
Expand Down
35 changes: 21 additions & 14 deletions lib/descope/api/v1/auth/otp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ module OTP
include Descope::Mixins::Common::EndpointsV1
include Descope::Mixins::Common::EndpointsV2

def otp_sign_in(method: nil, login_id: nil, login_options: nil, refresh_token: nil, provider_id: nil,
def otp_sign_in(method: nil, login_id: nil, login_options: nil, refresh_token: nil, provider_id: nil,
template_id: nil, sso_app_id: nil)
# Sign in (log in) an existing user with the unique login_id you provide. (See 'sign_up' function for an explanation of the
# login_id field.) Provide the DeliveryMethod required for this user. If the login_id value cannot be used for the
# Sign in (log in) an existing user with the unique login_id you provide.
# The login_id field is used to identify the user. It can be an email address or a phone number.
# Provide the DeliveryMethod required for this user. If the login_id value cannot be used for the
# DeliverMethod selected (for example, 'login_id = 4567qq445km' and 'DeliveryMethod = email')
validate_login_id(login_id)
uri = otp_compose_signin_url(method)
Expand All @@ -23,12 +24,15 @@ def otp_sign_in(method: nil, login_id: nil, login_options: nil, refresh_token: n
end

def otp_sign_up(method: nil, login_id: nil, user: {}, provider_id: nil, template_id: nil)
# Sign up (create) a new user using their email or phone number. Choose a delivery method for OTP
# verification, for example email, SMS, or WhatsApp.
# Sign up (create) a new user using their email or phone number.
# The login_id field is used to identify the user. It can be an email address or a phone number.
# Choose a delivery method for OTP verification, for example email, SMS, or Voice.
# (optional) Include additional user metadata that you wish to preserve.
user ||= {}
validate_login_id(login_id)

raise AuthException unless adjust_and_verify_delivery_method(method, login_id, user)
unless adjust_and_verify_delivery_method(method, login_id, user)
raise Descope::AuthException.new('Could not verify delivery method', code: 400)
end

uri = otp_compose_signup_url(method)
body = otp_compose_signup_body(method, login_id, user, provider_id, template_id)
Expand All @@ -38,9 +42,11 @@ def otp_sign_up(method: nil, login_id: nil, user: {}, provider_id: nil, template

def otp_sign_up_or_in(method: nil, login_id: nil, login_options: nil, provider_id: nil, template_id: nil,
sso_app_id: nil)
# Sign_up_or_in lets you handle both sign up and sign in with a single call. Sign-up_or_in will first
# determine if login_id is a new or existing end user. If login_id is new, a new end user user will be
# created and then authenticated using the OTP DeliveryMethod specified.
# Sign_up_or_in lets you handle both sign up and sign in with a single call.
# The login_id field is used to identify the user. It can be an email address or a phone number.
# Sign-up_or_in will first determine if login_id is a new or existing end user.
# If login_id is new, a new end user user will be created and then authenticated using the
# OTP DeliveryMethod specified.
# If login_id exists, the end user will be authenticated using the OTP DeliveryMethod specified.
validate_login_id(login_id)
uri = otp_compose_sign_up_or_in_url(method)
Expand Down Expand Up @@ -81,9 +87,10 @@ def otp_update_user_phone(
method: nil, login_id: nil, phone: nil, refresh_token: nil, add_to_login_ids: false,
on_merge_use_existing: false, provider_id: nil, template_id: nil
)
# Update the phone number of an existing end user, after verifying the authenticity of the end user using OTP.
# Update the phone number of an existing end user, after verifying the authenticity of the end user using OTP
validate_login_id(login_id)
validate_phone(method, phone)

uri = otp_compose_update_phone_url(method)
request_params = {
loginId: login_id,
Expand Down Expand Up @@ -127,7 +134,7 @@ def otp_compose_update_phone_url(method = nil)
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def otp_compose_signup_body(method, login_id, user, provider_id, template_id)
body = {
loginId: login_id,
loginId: login_id
}

unless user.nil?
Expand Down Expand Up @@ -167,7 +174,8 @@ def otp_compose_signin_body(login_id, login_options, provider_id, template_id, s
end

private
def otp_user_compose_update_body(login_id: nil, name: nil, phone: nil, email: nil, given_name: nil, middle_name: nil, family_name: nil)
def otp_user_compose_update_body(login_id: nil, name: nil, phone: nil, email: nil, given_name: nil,
middle_name: nil, family_name: nil)
user = {}
user[:loginId] = login_id if login_id
user[:name] = name if name
Expand All @@ -176,7 +184,6 @@ def otp_user_compose_update_body(login_id: nil, name: nil, phone: nil, email: ni
user[:givenName] = given_name if given_name
user[:middleName] = middle_name if middle_name
user[:familyName] = family_name if family_name

user
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/descope/mixins/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ module DeliveryMethod
WHATSAPP = 1
SMS = 2
EMAIL = 3
VOICE = 4
end

def get_method_string(method)
name = {
DeliveryMethod::WHATSAPP => 'whatsapp',
DeliveryMethod::SMS => 'sms',
DeliveryMethod::EMAIL => 'email'
DeliveryMethod::EMAIL => 'email',
DeliveryMethod::VOICE => 'voice'
}[method]

raise ArgumentException, "Unknown delivery method: #{method}" if name.nil?
Expand Down
27 changes: 21 additions & 6 deletions lib/descope/mixins/validation.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# frozen_string_literal: true

require 'descope/mixins/common'

module Descope
module Mixins
# Module to provide validation for specific data structures.
module Validation
include Descope::Mixins::Common
def validate_tenants(key_tenants)
raise ArgumentError, 'key_tenants should be an Array of hashes' unless key_tenants.is_a? Array

Expand Down Expand Up @@ -46,11 +49,18 @@ def validate_refresh_token_not_nil(refresh_token)
end

def validate_phone(method, phone)
phone_number_is_invalid = !phone.match?(PHONE_REGEX) unless phone.nil?

raise AuthException.new('Phone number cannot be empty', code: 400) unless phone.is_a?(String) && !phone.empty?
raise AuthException.new('Invalid phone number', code: 400) unless phone.match?(PHONE_REGEX)
raise AuthException.new('Invalid delivery method', code: 400) unless [
DeliveryMethod::WHATSAPP, DeliveryMethod::SMS
].include?(method)
raise AuthException.new("Invalid pattern for phone number: #{phone}", code: 400) if phone_number_is_invalid

valid_methods = DeliveryMethod.constants.map { |constant| DeliveryMethod.const_get(constant) }

# rubocop:disable Style/LineLength
unless valid_methods.include?(method)
valid_methods_names = valid_methods.map { |m| "DeliveryMethod::#{DeliveryMethod.constants[valid_methods.index(m)]}" }.join(', ')
raise AuthException.new("Delivery method should be one of the following: #{valid_methods_names}", code: 400)
end
end

def verify_provider(oauth_provider)
Expand All @@ -64,15 +74,20 @@ def validate_tenant(tenant)
end

def validate_redirect_url(return_url)
raise AuthException.new('Return_url cannot be empty', code: 400) unless return_url.is_a?(String) && !return_url.empty?
return if return_url.is_a?(String) && !return_url.empty?

raise AuthException.new('Return_url cannot be empty', code: 400)
end

def validate_code(code)
raise AuthException.new('Code cannot be empty', code: 400) unless code.is_a?(String) && !code.empty?
end

def validate_scim_group_id(group_id)
raise AuthException.new('SCIM Group ID cannot be empty', code: 400) unless group_id.is_a?(String) && !group_id.empty?
return if group_id.is_a?(String) && !group_id.empty?

raise AuthException.new('SCIM Group ID cannot be empty', code: 400)

end
end
end
Expand Down
Loading