Skip to content

Commit

Permalink
feat: public oauth (#735)
Browse files Browse the repository at this point in the history
* feat: oauth ruby
  • Loading branch information
manisha1997 authored Dec 11, 2024
1 parent 9877912 commit 57032c3
Show file tree
Hide file tree
Showing 33 changed files with 2,296 additions and 12 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/test-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ jobs:
TWILIO_API_SECRET: ${{ secrets.TWILIO_CLUSTER_TEST_API_KEY_SECRET }}
TWILIO_FROM_NUMBER: ${{ secrets.TWILIO_FROM_NUMBER }}
TWILIO_TO_NUMBER: ${{ secrets.TWILIO_TO_NUMBER }}
TWILIO_ORGS_ACCOUNT_SID: ${{ secrets.TWILIO_ORGS_ACCOUNT_SID }}
TWILIO_ORGS_CLIENT_SECRET: ${{ secrets.TWILIO_ORGS_CLIENT_SECRET }}
TWILIO_ORGS_CLIENT_ID: ${{ secrets.TWILIO_ORGS_CLIENT_ID }}
TWILIO_ORG_SID: ${{ secrets.TWILIO_ORG_SID }}
TWILIO_USER_SID: ${{ secrets.TWILIO_USER_SID }}
TWILIO_CLIENT_SECRET: ${{ secrets.TWILIO_CLIENT_SECRET }}
TWILIO_CLIENT_ID: ${{ secrets.TWILIO_CLIENT_ID }}
TWILIO_MESSAGE_SID: ${{ secrets.TWILIO_MESSAGE_SID }}
run: make cluster-test

- name: Fix code coverage paths
Expand Down
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Lint/AmbiguousBlockAssociation:
Metrics/BlockLength:
Exclude:
- 'spec/**/*'
- 'cluster/*'
- twilio-ruby.gemspec

Layout/HeredocIndentation:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ docker-push:
docker push twilio/twilio-ruby:${CURRENT_TAG}

cluster-test:
bundle exec rspec ./cluster_spec.rb
bundle exec rspec ./cluster

prettier:
bundle exec rubocop -A -d --cache true --parallel
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,11 @@ capability.add_scope(incoming_scope)

There is a slightly more detailed document in the [Capability][capability] section of the wiki.

## OAuth Feature for Twilio APIs
We are introducing Client Credentials Flow-based OAuth 2.0 authentication. This feature is currently in beta and its implementation is subject to change.

API examples [here](https://github.com/twilio/twilio-ruby/blob/main/examples/public_oauth.rb)

### Generate TwiML

To control phone calls, your application needs to output [TwiML][twiml].
Expand Down
19 changes: 19 additions & 0 deletions cluster/cluster_oauth_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require 'rspec/matchers'
require 'twilio-ruby'
require './lib/twilio-ruby/credential/client_credential_provider'

describe 'Cluster Test' do
before(:each) do
@account_sid = ENV['TWILIO_ACCOUNT_SID']
@client_secret = ENV['TWILIO_CLIENT_SECRET']
@client_id = ENV['TWILIO_CLIENT_ID']
@message_sid = ENV['TWILIO_MESSAGE_SID']
@credential = Twilio::REST::ClientCredentialProvider.new(@client_id, @client_secret)
@client = Twilio::REST::Client.new(@account_sid).credential_provider(@credential)
end

it 'can fetch a message' do
response = @client.messages(@message_sid).fetch
expect(response).to_not be_nil
end
end
40 changes: 40 additions & 0 deletions cluster/cluster_orgs_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'rspec/matchers'
require 'twilio-ruby'
require './lib/twilio-ruby/credential/client_credential_provider'

describe 'Cluster Test' do
before(:each) do
@client_secret = ENV['TWILIO_ORGS_CLIENT_SECRET']
@client_id = ENV['TWILIO_ORGS_CLIENT_ID']
@org_sid = ENV['TWILIO_ORG_SID']
@user_sid = ENV['TWILIO_USER_SID']
@account_sid = ENV['TWILIO_ORGS_ACCOUNT_SID']
@credential = Twilio::REST::ClientCredentialProvider.new(@client_id, @client_secret)
@client = Twilio::REST::Client.new.credential_provider(@credential)
end

it 'can list accounts' do
response = @client.preview_iam.organizations(@org_sid).accounts.list
expect(response).to_not be_nil
end

it 'can fetch specific account' do
response = @client.preview_iam.organizations(@org_sid).accounts(@account_sid).fetch
expect(response).to_not be_nil
end

it 'can access role assignments list' do
response = @client.preview_iam.organizations(@org_sid).role_assignments.list(scope: @account_sid)
expect(response).to_not be_nil
end

it 'can get user list' do
response = @client.preview_iam.organizations(@org_sid).users.list
expect(response).to_not be_nil
end

it 'can get single user' do
response = @client.preview_iam.organizations(@org_sid).users(@user_sid).fetch
expect(response).to_not be_nil
end
end
2 changes: 0 additions & 2 deletions cluster_spec.rb → cluster/cluster_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
require 'rspec/matchers'
require 'twilio-ruby'

# rubocop:disable Metrics/BlockLength
describe 'Cluster Test' do
before(:each) do
@account_sid = ENV['TWILIO_ACCOUNT_SID']
Expand Down Expand Up @@ -74,4 +73,3 @@
expect(@client.events.v1.sinks(sink.sid).delete).to eq(true)
end
end
# rubocop:enable Metrics/BlockLength
13 changes: 13 additions & 0 deletions examples/public_oauth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require 'twilio-ruby'
require 'twilio-ruby/credential/client_credential_provider'

credential_provider = Twilio::REST::ClientCredentialProvider.new(ENV['CLIENT_ID'], ENV['CLIENT_SECRET'])
# passing account sid is not mandatory
client = Twilio::REST::Client.new(ENV['ACCOUNT_SID']).credential_provider(credential_provider)

# send messages
client.messages.create(
from: ENV['TWILIO_PHONE_NUMBER'],
to: ENV['PHONE_NUMBER'],
body: 'Hello from Ruby!'
)
19 changes: 19 additions & 0 deletions lib/twilio-ruby/auth_strategy/auth_strategy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Twilio
module REST
class AuthStrategy
attr_accessor :auth_type

def initialize(auth_type)
@auth_type = auth_type
end

def auth_string
raise NotImplementedError, 'Subclasses must implement this method'
end

def requires_authentication
raise NotImplementedError, 'Subclasses must implement this method'
end
end
end
end
17 changes: 17 additions & 0 deletions lib/twilio-ruby/auth_strategy/no_auth_strategy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Twilio
module REST
class NoAuthStrategy < AuthStrategy
def initialize
super(AuthType::NONE)
end

def auth_string
''
end

def requires_authentication
false
end
end
end
end
39 changes: 39 additions & 0 deletions lib/twilio-ruby/auth_strategy/token_auth_strategy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require_relative 'auth_strategy'
require_relative './../credential/auth_type'
require 'jwt'
module Twilio
module REST
class TokenAuthStrategy < AuthStrategy
attr_accessor :token_manager, :token, :lock

def initialize(token_manager)
super(AuthType::ORGS_TOKEN)
@token = nil
@token_manager = token_manager
@lock = Mutex.new
end

def auth_string
token = fetch_token
"Bearer #{token}"
end

def fetch_token
@lock.synchronize do
@token = @token_manager.fetch_access_token if @token.nil? || token_expired? || @token == ''
return @token
end
end

def token_expired?
decoded_token = ::JWT.decode(@token, nil, false)
exp = decoded_token[0]['exp']
Time.at(exp) < Time.now
end

def requires_authentication
true
end
end
end
end
9 changes: 8 additions & 1 deletion lib/twilio-ruby/base/client_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class ClientBase
# rubocop:enable Style/ClassVars

attr_accessor :http_client, :username, :password, :account_sid, :auth_token, :region, :edge, :logger,
:user_agent_extensions
:user_agent_extensions, :credentials

# rubocop:disable Metrics/ParameterLists
def initialize(username = nil, password = nil, account_sid = nil, region = nil, http_client = nil, logger = nil,
Expand All @@ -22,6 +22,11 @@ def initialize(username = nil, password = nil, account_sid = nil, region = nil,
@logger = logger || Twilio.logger
@user_agent_extensions = user_agent_extensions || []
end

def credential_provider(credential_provider = nil)
@credentials = credential_provider
self
end
# rubocop:enable Metrics/ParameterLists

##
Expand All @@ -47,6 +52,8 @@ def request(host, port, method, uri, params = {}, data = {}, headers = {}, auth
@logger.debug("Request Params:#{params}")
end

auth = @credentials.to_auth_strategy.auth_string unless @credentials.nil?

response = @http_client.request(
host,
port,
Expand Down
17 changes: 17 additions & 0 deletions lib/twilio-ruby/credential/auth_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Twilio
module REST
class AuthType
BASIC = 'basic'
ORGS_TOKEN = 'orgs_token'
API_KEY = 'api_key'
NOAUTH = 'noauth'
CLIENT_CREDENTIALS = 'client_credentials'

def to_s
name
end
end
end
end
28 changes: 28 additions & 0 deletions lib/twilio-ruby/credential/client_credential_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require_relative 'credential_provider'
require_relative 'auth_type'
require_relative './../http/client_token_manager'
require_relative './../auth_strategy/token_auth_strategy'
module Twilio
module REST
class ClientCredentialProvider < CredentialProvider
attr_accessor :grant_type, :client_id, :client_secret, :orgs_token, :auth_strategy

def initialize(client_id, client_secret, orgs_token = nil)
super(AuthType::ORGS_TOKEN)
raise ArgumentError, 'client_id and client_secret are required' if client_id.nil? || client_secret.nil?

@grant_type = 'client_credentials'
@client_id = client_id
@client_secret = client_secret
@orgs_token = orgs_token
@auth_strategy = nil
end

def to_auth_strategy
@orgs_token = ClientTokenManager.new(@grant_type, @client_id, @client_secret) if @orgs_token.nil?
@auth_strategy = TokenAuthStrategy.new(@orgs_token) if @auth_strategy.nil?
@auth_strategy
end
end
end
end
11 changes: 11 additions & 0 deletions lib/twilio-ruby/credential/credential_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Twilio
module REST
class CredentialProvider
attr_accessor :auth_type

def initialize(auth_type)
@auth_type = auth_type
end
end
end
end
30 changes: 30 additions & 0 deletions lib/twilio-ruby/credential/orgs_credential_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

require_relative 'credential_provider'
require_relative 'auth_type'
require_relative './../http/org_token_manager'
require_relative './../auth_strategy/token_auth_strategy'
module Twilio
module REST
class OrgsCredentialProvider < CredentialProvider
attr_accessor :grant_type, :client_id, :client_secret, :orgs_token, :auth_strategy

def initialize(client_id, client_secret, orgs_token = nil)
super(AuthType::ORGS_TOKEN)
raise ArgumentError, 'client_id and client_secret are required' if client_id.nil? || client_secret.nil?

@grant_type = 'client_credentials'
@client_id = client_id
@client_secret = client_secret
@orgs_token = orgs_token
@auth_strategy = nil
end

def to_auth_strategy
@orgs_token = OrgTokenManager.new(@grant_type, @client_id, @client_secret) if @orgs_token.nil?
@auth_strategy = TokenAuthStrategy.new(@orgs_token) if @auth_strategy.nil?
@auth_strategy
end
end
end
end
1 change: 0 additions & 1 deletion lib/twilio-ruby/framework/rest/domain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ def absolute_url(uri)

def request(method, uri, params = {}, data = {}, headers = {}, auth = nil, timeout = nil)
url = uri.match(/^http/) ? uri : absolute_url(uri)

@client.request(
@base_url,
@port,
Expand Down
1 change: 1 addition & 0 deletions lib/twilio-ruby/framework/rest/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def process_response(response)
end

def load_page(payload)
return payload['Resources'] if payload['Resources']
if payload['meta'] && payload['meta']['key']
return payload[payload['meta']['key']]
else
Expand Down
28 changes: 28 additions & 0 deletions lib/twilio-ruby/http/client_token_manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Twilio
module REST
class ClientTokenManager
attr_accessor :grant_type, :client_id, :client_secret, :code, :redirect_uri, :audience, :refresh_token, :scope

def initialize(grant_type, client_id, client_secret, code = nil, redirect_uri = nil, audience = nil,
refresh_token = nil, scope = nil)
raise ArgumentError, 'client_id and client_secret are required' if client_id.nil? || client_secret.nil?

@grant_type = grant_type
@client_id = client_id
@client_secret = client_secret
@code = code
@redirect_uri = redirect_uri
@audience = audience
@refresh_token = refresh_token
@scope = scope
end

def fetch_access_token
client = Twilio::REST::Client.new
token_instance = client.preview_iam.v1.token.create(grant_type: @grant_type,
client_id: @client_id, client_secret: @client_secret)
token_instance.access_token
end
end
end
end
Loading

0 comments on commit 57032c3

Please sign in to comment.