# frozen_string_literal: true require_relative '../spec_helper' module Aws describe SSOTokenProvider do let(:client) do SSOOIDC::Client.new( region: 'us-west-2', stub_responses: true ) end let(:now) { Time.now } let(:in_one_hour) { now + 60 * 60 } let(:one_hour_ago) { now - 60 * 60 } let(:expiration) { in_one_hour } let(:access_token) { 'token' } let(:cached_token) do { 'accessToken' => access_token, 'expiresAt' => expiration } end let(:sso_session) { 'session' } let(:sso_region) { 'us-west-2' } let(:sso_opts) do { sso_session: sso_session, sso_region: sso_region, } end def mock_token_file(sso_session, cached_token) sso_session_sha1 = OpenSSL::Digest::SHA1.hexdigest(sso_session.encode('utf-8')) allow(Dir).to receive(:home).and_return('HOME') path = File.join(Dir.home, '.aws', 'sso', 'cache', "#{sso_session_sha1}.json") allow(File).to receive(:read).with(path).and_return( JSON.dump(cached_token) ) end def expect_token_write_back(sso_session, expected_token) sso_session_sha1 = OpenSSL::Digest::SHA1.hexdigest(sso_session.encode('utf-8')) allow(Dir).to receive(:home).and_return('HOME') path = File.join(Dir.home, '.aws', 'sso', 'cache', "#{sso_session_sha1}.json") expect(File).to receive(:write) do |arg1, arg2| expect(arg1).to eq(path) expect(Json.load(arg2)).to eq(expected_token) end end describe '#initialize' do it 'constructs an client with passed arguments when not given' do expect(SSOOIDC::Client).to receive(:new) .with({region: sso_region, credentials: nil, token_provider: nil}) .and_return(client) mock_token_file(sso_session, cached_token) provider = SSOTokenProvider.new(sso_opts) expect(provider.client).to be(client) end it 'raises an argument error when arguments are missing' do expect { SSOTokenProvider.new }.to raise_error( ArgumentError, /Missing required keys/ ) end it 'raises an InvalidSSOToken error when token file is missing' do expect(SSOOIDC::Client).to receive(:new) .with({region: sso_region, credentials: nil, token_provider: nil}) .and_return(client) sso_session_sha1 = OpenSSL::Digest::SHA1.hexdigest(sso_session.encode('utf-8')) allow(Dir).to receive(:home).and_return('HOME') path = File.join(Dir.home, '.aws', 'sso', 'cache', "#{sso_session_sha1}.json") allow(File).to receive(:read).with(path).and_raise(Errno::ENOENT) expect do SSOTokenProvider.new(sso_opts) end.to raise_error(Aws::Errors::InvalidSSOToken) end it 'raises an InvalidSSOCredentials error when token file is missing fields' do mock_token_file(sso_session, {'accessToken' => access_token}) expect do SSOTokenProvider.new(sso_opts) end.to raise_error(Aws::Errors::InvalidSSOToken) end it 'sets the client when passed in and does not create a new one' do test_client = client # force construction expect(SSOOIDC::Client).not_to receive(:new) mock_token_file(sso_session, cached_token) provider = SSOTokenProvider.new(sso_opts.merge(client: test_client)) expect(provider.client).to be(test_client) end end describe '#sso_cache_file' do it 'produces the correct file location for a basic name' do test_client = client # force construction sso_session = 'admin' mock_token_file(sso_session, cached_token) provider = SSOTokenProvider.new(sso_opts.merge(sso_session: sso_session, client: test_client)) expect(File.basename(provider.send(:sso_cache_file))).to eq('d033e22ae348aeb5660fc2140aec35850c4da997.json') end it 'produces the correct file location for a session with a dash' do test_client = client # force construction sso_session = 'dev-scopes' mock_token_file(sso_session, cached_token) provider = SSOTokenProvider.new(sso_opts.merge(sso_session: sso_session, client: test_client)) expect(File.basename(provider.send(:sso_cache_file))).to eq('75e4d41276d8bd17f85986fc6cccef29fd725ce3.json') end end describe '#refresh' do let(:sso_session) { 'session' } let(:sso_region) { 'us-west-2' } let(:sso_opts) do { sso_session: sso_session, sso_region: sso_region, client: client } end let(:now) { Time.parse('2021-12-25T13:30:00Z') } before do allow(Time).to receive(:now).and_return(now) sso_opts # force initialization of client to avoid File read of partitions.json end context 'valid token with all fields' do let(:cached_token) do Json.load(<<~JSON) { "startUrl": "https://d-123.awsapps.com/start", "region": "us-west-2", "accessToken": "cachedtoken", "expiresAt": "2021-12-25T21:30:00Z", "clientId": "clientid", "clientSecret": "YSBzZWNyZXQ=", "registrationExpiresAt": "2022-12-25T13:30:00Z", "refreshToken": "cachedrefreshtoken" } JSON end it 'returns the token' do mock_token_file(sso_session, cached_token) provider = SSOTokenProvider.new(sso_opts) expect(provider.token.token).to eq('cachedtoken') expect(provider.token.expiration).to eq(Time.parse('2021-12-25T21:30:00Z')) end end context 'minimal valid cached token' do let(:cached_token) do Json.load(<<~JSON) { "accessToken": "cachedtoken", "expiresAt": "2021-12-25T21:30:00Z" } JSON end it 'returns the token' do mock_token_file(sso_session, cached_token) provider = SSOTokenProvider.new(sso_opts) expect(provider.token.token).to eq('cachedtoken') expect(provider.token.expiration).to eq(Time.parse('2021-12-25T21:30:00Z')) end end context 'minimal expired cached token' do let(:cached_token) do Json.load(<<~JSON) { "accessToken": "cachedtoken", "expiresAt": "2021-12-25T13:00:00Z" } JSON end it 'raises an exception' do mock_token_file(sso_session, cached_token) expect do SSOTokenProvider.new(sso_opts) end.to raise_error(Aws::Errors::InvalidSSOToken) end end context 'Token missing the expiresAt field' do let(:cached_token) do Json.load(<<~JSON) { "accessToken": "cachedtoken" } JSON end it 'raises an exception' do mock_token_file(sso_session, cached_token) expect do SSOTokenProvider.new(sso_opts) end.to raise_error(Aws::Errors::InvalidSSOToken) end end context 'Token missing the accessToken field' do let(:cached_token) do Json.load(<<~JSON) { "expiresAt": "2021-12-25T21:30:00Z" } JSON end it 'raises an exception' do mock_token_file(sso_session, cached_token) expect do SSOTokenProvider.new(sso_opts) end.to raise_error(Aws::Errors::InvalidSSOToken) end end context 'Expired token refresh with refresh token' do let(:cached_token) do Json.load(<<~JSON) { "startUrl": "https://d-123.awsapps.com/start", "region": "us-west-2", "accessToken": "cachedtoken", "expiresAt": "2021-12-25T13:00:00Z", "clientId": "clientid", "clientSecret": "YSBzZWNyZXQ=", "registrationExpiresAt": "2022-12-25T13:30:00Z", "refreshToken": "cachedrefreshtoken" } JSON end let(:execpted_token_write_back) do Json.load(<<~JSON) { "startUrl": "https://d-123.awsapps.com/start", "region": "us-west-2", "accessToken": "newtoken", "expiresAt": "2021-12-25T21:30:00Z", "clientId": "clientid", "clientSecret": "YSBzZWNyZXQ=", "registrationExpiresAt": "2022-12-25T13:30:00Z", "refreshToken": "newrefreshtoken" } JSON end it 'refreshes the token' do mock_token_file(sso_session, cached_token) expect(client).to receive(:create_token).with( grant_type: 'refresh_token', client_id: cached_token['clientId'], client_secret: cached_token['clientSecret'], refresh_token: cached_token['refreshToken'] ).and_return(double('Response', token_type: 'Bearer', access_token: 'newtoken', expires_in: 28800, refresh_token: 'newrefreshtoken')) expect_token_write_back(sso_session, execpted_token_write_back) provider = SSOTokenProvider.new(sso_opts) expect(provider.token.token).to eq('newtoken') expect(provider.token.expiration).to eq(Time.parse('2021-12-25T21:30:00Z')) end end context 'Expired token refresh without new refresh token' do let(:cached_token) do Json.load(<<~JSON) { "startUrl": "https://d-123.awsapps.com/start", "region": "us-west-2", "accessToken": "cachedtoken", "expiresAt": "2021-12-25T13:00:00Z", "clientId": "clientid", "clientSecret": "YSBzZWNyZXQ=", "registrationExpiresAt": "2022-12-25T13:30:00Z", "refreshToken": "cachedrefreshtoken" } JSON end let(:execpted_token_write_back) do Json.load(<<~JSON) { "startUrl": "https://d-123.awsapps.com/start", "region": "us-west-2", "accessToken": "newtoken", "expiresAt": "2021-12-25T21:30:00Z", "clientId": "clientid", "clientSecret": "YSBzZWNyZXQ=", "registrationExpiresAt": "2022-12-25T13:30:00Z" } JSON end it 'refreshes the token' do mock_token_file(sso_session, cached_token) expect(client).to receive(:create_token).with( grant_type: 'refresh_token', client_id: cached_token['clientId'], client_secret: cached_token['clientSecret'], refresh_token: cached_token['refreshToken'] ).and_return(double('Response', token_type: 'Bearer', access_token: 'newtoken', expires_in: 28800, refresh_token: nil)) expect_token_write_back(sso_session, execpted_token_write_back) provider = SSOTokenProvider.new(sso_opts) expect(provider.token.token).to eq('newtoken') expect(provider.token.expiration).to eq(Time.parse('2021-12-25T21:30:00Z')) end end context 'Expired token and expired client registration' do let(:cached_token) do Json.load(<<~JSON) { "startUrl": "https://d-123.awsapps.com/start", "region": "us-west-2", "accessToken": "cachedtoken", "expiresAt": "2021-10-25T13:00:00Z", "clientId": "clientid", "clientSecret": "YSBzZWNyZXQ=", "registrationExpiresAt": "2021-11-25T13:30:00Z", "refreshToken": "cachedrefreshtoken" } JSON end it 'raises an exception' do mock_token_file(sso_session, cached_token) expect(client).not_to receive(:create_token) expect do SSOTokenProvider.new(sso_opts) end.to raise_error(Aws::Errors::InvalidSSOToken) end end end end end