Skip to content
Draft
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/pkg/
/spec/reports/
/tmp/
/vendor/

.DS_Store
.rspec_status
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ If you don't have an account you can [sign up for a free developer account here]
|region| Optional: `us` or `eu`. Defaults to `us` |
|max_results| Optional: Defaults to 1000 |
|timeout| Optional: Defaults to 60 (requires httparty > 0.16.2) |
|token_expiration_buffer| Optional: Number of seconds before token expiration to trigger refresh. Defaults to 30 |

```ruby
require 'onelogin'
Expand Down
17 changes: 14 additions & 3 deletions lib/onelogin/api/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Client
Nokogiri::XML::ParseOptions::NONET

DEFAULT_USER_AGENT = "onelogin-ruby-sdk v#{OneLogin::VERSION}".freeze
DEFAULT_TOKEN_EXPIRATION_BUFFER = 30 # seconds

# Create a new instance of the Client.
#
Expand All @@ -38,6 +39,7 @@ def initialize(config)
@client_secret = options[:client_secret]
@region = options[:region] || 'us'
@max_results = options[:max_results] || 1000
@token_expiration_buffer = options[:token_expiration_buffer] || DEFAULT_TOKEN_EXPIRATION_BUFFER

if options[:timeout] and defined? self.class.default_timeout
self.class.default_timeout options[:timeout]
Expand Down Expand Up @@ -67,17 +69,26 @@ def clean_error
end

def expired?
Time.now.utc > @expiration
return true if @expiration.nil?
Time.now.utc > (@expiration - @token_expiration_buffer)
end

def prepare_token
if @access_token.nil?
access_token
get_new_token
elsif expired?
regenerate_token
# Try to regenerate token, fall back to getting new token if regeneration fails
regenerate_token || get_new_token
end
end

# Internal method to get a new access token
# This is separate from the public access_token method to allow internal use
#
def get_new_token
access_token
end
Comment on lines +85 to +90
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method simply delegates to access_token without adding any value. Consider directly calling access_token in prepare_token instead of introducing this wrapper method, or add meaningful functionality if this separation is intended for future extensibility.

Copilot uses AI. Check for mistakes.

def handle_operation_response(response)
result = false
begin
Expand Down
1 change: 1 addition & 0 deletions onelogin.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "bundler"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "webmock", "~> 3.0"
end
209 changes: 209 additions & 0 deletions spec/lib/onelogin/api/token_expiration_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
require "spec_helper"
require "webmock/rspec"

RSpec.describe "Token Expiration Handling" do
let(:client_id) { 'test_client_id' }
let(:client_secret) { 'test_client_secret' }
let(:region) { 'us' }
let(:token_url) { 'https://api.us.onelogin.com/auth/oauth2/v2/token' }

let(:test_time) { Time.utc(2025, 1, 1, 12, 0, 0) }

let(:valid_token_response) {
{
access_token: 'test_access_token',
refresh_token: 'test_refresh_token',
token_type: 'bearer',
expires_in: 36000,
created_at: test_time.iso8601
}.to_json
}

let(:refreshed_token_response) {
{
access_token: 'refreshed_access_token',
refresh_token: 'refreshed_refresh_token',
token_type: 'bearer',
expires_in: 36000,
created_at: test_time.iso8601
}.to_json
}

before(:each) do
WebMock.disable_net_connect!(allow_localhost: true)
end

after(:each) do
WebMock.reset!
end

context 'when token is nil' do
it 'expired? should return true' do
client = OneLogin::Api::Client.new(
client_id: client_id,
client_secret: client_secret,
region: region
)

expect(client.send(:expired?)).to be true
end

it 'prepare_token should get a new token' do
client = OneLogin::Api::Client.new(
client_id: client_id,
client_secret: client_secret,
region: region
)

stub_request(:post, token_url)
.with(
body: { 'grant_type' => 'client_credentials' }.to_json,
headers: {
'Authorization' => "client_id:#{client_id},client_secret:#{client_secret}",
'Content-Type' => 'application/json'
}
)
.to_return(status: 200, body: valid_token_response, headers: {})

client.send(:prepare_token)

expect(client.instance_variable_get(:@access_token)).to eq('test_access_token')
end
end

context 'when token is about to expire' do
it 'expired? should return true when within expiration buffer' do
client = OneLogin::Api::Client.new(
client_id: client_id,
client_secret: client_secret,
region: region,
token_expiration_buffer: 30
)

# Set token to expire in 25 seconds (less than 30 second buffer)
client.instance_variable_set(:@access_token, 'test_token')
client.instance_variable_set(:@refresh_token, 'test_refresh')
client.instance_variable_set(:@expiration, Time.now.utc + 25)

expect(client.send(:expired?)).to be true
end

it 'expired? should return false when outside expiration buffer' do
client = OneLogin::Api::Client.new(
client_id: client_id,
client_secret: client_secret,
region: region,
token_expiration_buffer: 30
)

# Set token to expire in 60 seconds (more than 30 second buffer)
client.instance_variable_set(:@access_token, 'test_token')
client.instance_variable_set(:@refresh_token, 'test_refresh')
client.instance_variable_set(:@expiration, Time.now.utc + 60)

expect(client.send(:expired?)).to be false
end

it 'prepare_token should regenerate token when expired' do
client = OneLogin::Api::Client.new(
client_id: client_id,
client_secret: client_secret,
region: region,
token_expiration_buffer: 30
)

# Set token to expire in 25 seconds
client.instance_variable_set(:@access_token, 'old_token')
client.instance_variable_set(:@refresh_token, 'old_refresh')
client.instance_variable_set(:@expiration, Time.now.utc + 25)

stub_request(:post, token_url)
.with(
body: {
'grant_type' => 'refresh_token',
'access_token' => 'old_token',
'refresh_token' => 'old_refresh'
}.to_json,
headers: {
'Content-Type' => 'application/json'
}
)
.to_return(status: 200, body: refreshed_token_response, headers: {})

client.send(:prepare_token)

expect(client.instance_variable_get(:@access_token)).to eq('refreshed_access_token')
end
end

context 'when token regeneration fails' do
it 'should fall back to getting a new token' do
client = OneLogin::Api::Client.new(
client_id: client_id,
client_secret: client_secret,
region: region,
token_expiration_buffer: 30
)

# Set token to be expired
client.instance_variable_set(:@access_token, 'old_token')
client.instance_variable_set(:@refresh_token, 'old_refresh')
client.instance_variable_set(:@expiration, Time.now.utc + 25)

# First request: regenerate_token fails with 401
stub_request(:post, token_url)
.with(
body: {
'grant_type' => 'refresh_token',
'access_token' => 'old_token',
'refresh_token' => 'old_refresh'
}.to_json
)
.to_return(status: 401, body: { error: 'invalid_token' }.to_json, headers: {})

# Second request: get new token succeeds
stub_request(:post, token_url)
.with(
body: { 'grant_type' => 'client_credentials' }.to_json,
headers: {
'Authorization' => "client_id:#{client_id},client_secret:#{client_secret}",
'Content-Type' => 'application/json'
}
)
.to_return(status: 200, body: valid_token_response, headers: {})

client.send(:prepare_token)

# Should have new token, not the old one
expect(client.instance_variable_get(:@access_token)).to eq('test_access_token')
end
end

context 'configurable token expiration buffer' do
it 'allows custom expiration buffer' do
client = OneLogin::Api::Client.new(
client_id: client_id,
client_secret: client_secret,
region: region,
token_expiration_buffer: 60
)

# Set token to expire in 45 seconds (less than 60 second buffer)
client.instance_variable_set(:@access_token, 'test_token')
client.instance_variable_set(:@refresh_token, 'test_refresh')
client.instance_variable_set(:@expiration, Time.now.utc + 45)

expect(client.send(:expired?)).to be true
end

it 'uses default buffer of 30 seconds if not specified' do
client = OneLogin::Api::Client.new(
client_id: client_id,
client_secret: client_secret,
region: region
)

expect(client.instance_variable_get(:@token_expiration_buffer)).to eq(30)
end
end
end
29 changes: 29 additions & 0 deletions vendor/bundle/ruby/3.2.0/bin/htmldiff
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env ruby3.2
#
# This file was generated by RubyGems.
#
# The application 'diff-lcs' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

Gem.use_gemdeps

version = ">= 0.a"

str = ARGV.first
if str
str = str.b[/\A_(.*)_\z/, 1]
if str and Gem::Version.correct?(str)
version = str
ARGV.shift
end
end

if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('diff-lcs', 'htmldiff', version)
else
gem "diff-lcs", version
load Gem.bin_path("diff-lcs", "htmldiff", version)
end
29 changes: 29 additions & 0 deletions vendor/bundle/ruby/3.2.0/bin/httparty
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env ruby3.2
#
# This file was generated by RubyGems.
#
# The application 'httparty' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

Gem.use_gemdeps

version = ">= 0.a"

str = ARGV.first
if str
str = str.b[/\A_(.*)_\z/, 1]
if str and Gem::Version.correct?(str)
version = str
ARGV.shift
end
end

if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('httparty', 'httparty', version)
else
gem "httparty", version
load Gem.bin_path("httparty", "httparty", version)
end
29 changes: 29 additions & 0 deletions vendor/bundle/ruby/3.2.0/bin/ldiff
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env ruby3.2
#
# This file was generated by RubyGems.
#
# The application 'diff-lcs' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

Gem.use_gemdeps

version = ">= 0.a"

str = ARGV.first
if str
str = str.b[/\A_(.*)_\z/, 1]
if str and Gem::Version.correct?(str)
version = str
ARGV.shift
end
end

if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('diff-lcs', 'ldiff', version)
else
gem "diff-lcs", version
load Gem.bin_path("diff-lcs", "ldiff", version)
end
29 changes: 29 additions & 0 deletions vendor/bundle/ruby/3.2.0/bin/nokogiri
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env ruby3.2
#
# This file was generated by RubyGems.
#
# The application 'nokogiri' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

Gem.use_gemdeps

version = ">= 0.a"

str = ARGV.first
if str
str = str.b[/\A_(.*)_\z/, 1]
if str and Gem::Version.correct?(str)
version = str
ARGV.shift
end
end

if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('nokogiri', 'nokogiri', version)
else
gem "nokogiri", version
load Gem.bin_path("nokogiri", "nokogiri", version)
end
Loading