require 'spec_helper' module Doorkeeper describe AccessToken do let(:clazz) { Doorkeeper::AccessToken } subject { FactoryBot.build(:access_token) } it { expect(subject).to be_valid } it_behaves_like 'an accessible token' it_behaves_like 'a revocable token' it_behaves_like 'a unique token' do let(:factory_name) { :access_token } end module CustomGeneratorArgs def self.generate; end end describe :generate_token do it 'generates a token using the default method' do FactoryBot.create :access_token token = FactoryBot.create :access_token expect(token.token).to be_a(String) end context 'with hashing enabled' do let(:token) { FactoryBot.create :access_token } include_context 'with token hashing enabled' it 'holds a volatile plaintext token when created' do expect(token.plaintext_token).to be_a(String) expect(token.token) .to eq(hashed_or_plain_token_func.call(token.plaintext_token)) # Finder method only finds the hashed token loaded = clazz.find_by(token: token.token) expect(loaded).to eq(token) expect(loaded.plaintext_token).to be_nil expect(loaded.token).to eq(token.token) end it 'does not find_by plain text tokens' do expect(clazz.find_by(token: token.plaintext_token)).to be_nil end describe 'with having a plain text token' do let(:plain_text_token) { 'plain text token' } let(:access_token) { FactoryBot.create :access_token } before do # Assume we have a plain text token from before activating the option access_token.update_column(:token, plain_text_token) end context 'without fallback lookup' do it 'does not provide lookups with either through by_token' do expect(clazz.by_token(plain_text_token)).to eq(nil) expect(clazz.by_token(access_token.token)).to eq(nil) # And it does not touch the token access_token.reload expect(access_token.token).to eq(plain_text_token) end end context 'with fallback lookup' do include_context 'with token hashing and fallback lookup enabled' it 'upgrades a plain token when falling back to it' do # Side-effect: This will automatically upgrade the token expect(clazz).to receive(:upgrade_fallback_value).and_call_original expect(clazz.by_token(plain_text_token)).to eq(access_token) # Will find subsequently by hashing the token expect(clazz.by_token(plain_text_token)).to eq(access_token) # And it modifies the token value access_token.reload expect(access_token.token).not_to eq(plain_text_token) expect(clazz.find_by(token: plain_text_token)).to eq(nil) expect(clazz.find_by(token: access_token.token)).not_to be_nil end end end end it 'generates a token using a custom object' do eigenclass = class << CustomGeneratorArgs; self; end eigenclass.class_eval do remove_method :generate end module CustomGeneratorArgs def self.generate(opts = {}) "custom_generator_token_#{opts[:resource_owner_id]}" end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "Doorkeeper::CustomGeneratorArgs" end token = FactoryBot.create :access_token expect(token.token).to match(/custom_generator_token_\d+/) end it 'allows the custom generator to access the application details' do eigenclass = class << CustomGeneratorArgs; self; end eigenclass.class_eval do remove_method :generate end module CustomGeneratorArgs def self.generate(opts = {}) "custom_generator_token_#{opts[:application].name}" end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "Doorkeeper::CustomGeneratorArgs" end token = FactoryBot.create :access_token expect(token.token).to match(/custom_generator_token_Application \d+/) end it 'allows the custom generator to access the scopes' do eigenclass = class << CustomGeneratorArgs; self; end eigenclass.class_eval do remove_method :generate end module CustomGeneratorArgs def self.generate(opts = {}) "custom_generator_token_#{opts[:scopes].count}_#{opts[:scopes]}" end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "Doorkeeper::CustomGeneratorArgs" end token = FactoryBot.create :access_token, scopes: 'public write' expect(token.token).to eq 'custom_generator_token_2_public write' end it 'allows the custom generator to access the expiry length' do eigenclass = class << CustomGeneratorArgs; self; end eigenclass.class_eval do remove_method :generate end module CustomGeneratorArgs def self.generate(opts = {}) "custom_generator_token_#{opts[:expires_in]}" end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "Doorkeeper::CustomGeneratorArgs" end token = FactoryBot.create :access_token expect(token.token).to eq 'custom_generator_token_7200' end it 'allows the custom generator to access the created time' do module CustomGeneratorArgs def self.generate(opts = {}) "custom_generator_token_#{opts[:created_at].to_i}" end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "Doorkeeper::CustomGeneratorArgs" end token = FactoryBot.create :access_token created_at = token.created_at expect(token.token).to eq "custom_generator_token_#{created_at.to_i}" end it 'raises an error if the custom object does not support generate' do module NoGenerate end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "Doorkeeper::NoGenerate" end expect { FactoryBot.create :access_token }.to( raise_error(Doorkeeper::Errors::UnableToGenerateToken) ) end it 'raises original error if something went wrong in custom generator' do eigenclass = class << CustomGeneratorArgs; self; end eigenclass.class_eval do remove_method :generate end module CustomGeneratorArgs def self.generate(_opts = {}) raise LoadError, 'custom behaviour' end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "Doorkeeper::CustomGeneratorArgs" end expect { FactoryBot.create :access_token }.to( raise_error(LoadError) ) end it 'raises an error if the custom object does not exist' do Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "Doorkeeper::NotReal" end expect { FactoryBot.create :access_token }.to( raise_error(Doorkeeper::Errors::TokenGeneratorNotFound, /NotReal/) ) end end describe :refresh_token do it 'has empty refresh token if it was not required' do token = FactoryBot.create :access_token expect(token.refresh_token).to be_nil end it 'generates a refresh token if it was requested' do token = FactoryBot.create :access_token, use_refresh_token: true expect(token.refresh_token).not_to be_nil end it 'is not valid if token exists' do token1 = FactoryBot.create :access_token, use_refresh_token: true token2 = FactoryBot.create :access_token, use_refresh_token: true token2.refresh_token = token1.refresh_token expect(token2).not_to be_valid end it 'expects database to raise an error if refresh tokens are the same' do token1 = FactoryBot.create :access_token, use_refresh_token: true token2 = FactoryBot.create :access_token, use_refresh_token: true expect do token2.refresh_token = token1.refresh_token token2.save(validate: false) end.to raise_error(uniqueness_error) end context 'with hashing enabled' do include_context 'with token hashing enabled' let(:token) { FactoryBot.create :access_token, use_refresh_token: true } it 'holds a volatile refresh token when created' do expect(token.plaintext_refresh_token).to be_a(String) expect(token.refresh_token) .to eq(hashed_or_plain_token_func.call(token.plaintext_refresh_token)) # Finder method only finds the hashed token loaded = clazz.find_by(refresh_token: token.refresh_token) expect(loaded).to eq(token) expect(loaded.plaintext_refresh_token).to be_nil expect(loaded.refresh_token).to eq(token.refresh_token) end it 'does not find_by plain text refresh tokens' do expect(clazz.find_by(refresh_token: token.plaintext_refresh_token)).to be_nil end describe 'with having a plain text token' do let(:plain_refresh_token) { 'plain refresh token' } let(:access_token) { FactoryBot.create :access_token } before do # Assume we have a plain text token from before activating the option access_token.update_column(:refresh_token, plain_refresh_token) end context 'without fallback lookup' do it 'does not provide lookups with either through by_token' do expect(clazz.by_refresh_token(plain_refresh_token)).to eq(nil) expect(clazz.by_refresh_token(access_token.refresh_token)).to eq(nil) # And it does not touch the token access_token.reload expect(access_token.refresh_token).to eq(plain_refresh_token) end end context 'with fallback lookup' do include_context 'with token hashing and fallback lookup enabled' it 'upgrades a plain token when falling back to it' do # Side-effect: This will automatically upgrade the token expect(clazz).to receive(:upgrade_fallback_value).and_call_original expect(clazz.by_refresh_token(plain_refresh_token)).to eq(access_token) # Will find subsequently by hashing the token expect(clazz.by_refresh_token(plain_refresh_token)).to eq(access_token) # And it modifies the token value access_token.reload expect(access_token.refresh_token).not_to eq(plain_refresh_token) expect(clazz.find_by(refresh_token: plain_refresh_token)).to eq(nil) expect(clazz.find_by(refresh_token: access_token.refresh_token)).not_to be_nil end end end end end describe 'validations' do it 'is valid without resource_owner_id' do # For client credentials flow subject.resource_owner_id = nil expect(subject).to be_valid end it 'is valid without application_id' do # For resource owner credentials flow subject.application_id = nil expect(subject).to be_valid end end describe '#same_credential?' do context 'with default parameters' do let(:resource_owner_id) { 100 } let(:application) { FactoryBot.create :application } let(:default_attributes) do { application: application, resource_owner_id: resource_owner_id } end let(:access_token1) { FactoryBot.create :access_token, default_attributes } context 'the second token has the same owner and same app' do let(:access_token2) { FactoryBot.create :access_token, default_attributes } it 'success' do expect(access_token1.same_credential?(access_token2)).to be_truthy end end context 'the second token has same owner and different app' do let(:other_application) { FactoryBot.create :application } let(:access_token2) do FactoryBot.create :access_token, application: other_application, resource_owner_id: resource_owner_id end it 'fail' do expect(access_token1.same_credential?(access_token2)).to be_falsey end end context 'the second token has different owner and different app' do let(:other_application) { FactoryBot.create :application } let(:access_token2) do FactoryBot.create :access_token, application: other_application, resource_owner_id: 42 end it 'fail' do expect(access_token1.same_credential?(access_token2)).to be_falsey end end context 'the second token has different owner and same app' do let(:access_token2) do FactoryBot.create :access_token, application: application, resource_owner_id: 42 end it 'fail' do expect(access_token1.same_credential?(access_token2)).to be_falsey end end end end describe '#acceptable?' do context 'a token that is not accessible' do let(:token) { FactoryBot.create(:access_token, created_at: 6.hours.ago) } it 'should return false' do expect(token.acceptable?(nil)).to be false end end context 'a token that has the incorrect scopes' do let(:token) { FactoryBot.create(:access_token) } it 'should return false' do expect(token.acceptable?(['public'])).to be false end end context 'a token is acceptable with the correct scopes' do let(:token) do token = FactoryBot.create(:access_token) token[:scopes] = 'public' token end it 'should return true' do expect(token.acceptable?(['public'])).to be true end end end describe '.revoke_all_for' do let(:resource_owner) { double(id: 100) } let(:application) { FactoryBot.create :application } let(:default_attributes) do { application: application, resource_owner_id: resource_owner.id } end it 'revokes all tokens for given application and resource owner' do FactoryBot.create :access_token, default_attributes AccessToken.revoke_all_for application.id, resource_owner AccessToken.all.each do |token| expect(token).to be_revoked end end it 'matches application' do access_token_for_different_app = FactoryBot.create( :access_token, default_attributes.merge(application: FactoryBot.create(:application)) ) AccessToken.revoke_all_for application.id, resource_owner expect(access_token_for_different_app.reload).not_to be_revoked end it 'matches resource owner' do access_token_for_different_owner = FactoryBot.create( :access_token, default_attributes.merge(resource_owner_id: 90) ) AccessToken.revoke_all_for application.id, resource_owner expect(access_token_for_different_owner.reload).not_to be_revoked end end describe '.matching_token_for' do let(:resource_owner_id) { 100 } let(:application) { FactoryBot.create :application } let(:scopes) { Doorkeeper::OAuth::Scopes.from_string('public write') } let(:default_attributes) do { application: application, resource_owner_id: resource_owner_id, scopes: scopes.to_s } end before do default_scopes_exist(*scopes.all) end it 'returns only one token' do token = FactoryBot.create :access_token, default_attributes last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to eq(token) end it 'accepts resource owner as object' do resource_owner = double(to_key: true, id: 100) token = FactoryBot.create :access_token, default_attributes last_token = AccessToken.matching_token_for(application, resource_owner, scopes) expect(last_token).to eq(token) end it 'accepts nil as resource owner' do token = FactoryBot.create :access_token, default_attributes.merge(resource_owner_id: nil) last_token = AccessToken.matching_token_for(application, nil, scopes) expect(last_token).to eq(token) end it 'excludes revoked tokens' do FactoryBot.create :access_token, default_attributes.merge(revoked_at: 1.day.ago) last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to be_nil end it "excludes tokens with a different application" do FactoryBot.create :access_token, default_attributes.merge(application: FactoryBot.create(:application)) last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to be_nil end it "excludes tokens with a different resource owner" do FactoryBot.create :access_token, default_attributes.merge(resource_owner_id: 2) last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to be_nil end it "excludes tokens with fewer scopes" do FactoryBot.create :access_token, default_attributes.merge(scopes: 'public') last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to be_nil end it 'excludes tokens with different scopes' do FactoryBot.create :access_token, default_attributes.merge(scopes: 'public email') last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to be_nil end it 'excludes tokens with additional scopes' do FactoryBot.create :access_token, default_attributes.merge(scopes: 'public write email') last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to be_nil end it 'excludes tokens with scopes that are not present in server scopes' do FactoryBot.create :access_token, default_attributes.merge( application: application, scopes: 'public read' ) last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to be_nil end it 'excludes tokens with scopes that are not present in application scopes' do application = FactoryBot.create :application, scopes: "private read" FactoryBot.create :access_token, default_attributes.merge( application: application ) last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to be_nil end it 'does not match token if empty scope requested and token/app scopes present' do application = FactoryBot.create :application, scopes: "sample:scope" app_params = { application_id: application.id, scopes: "sample:scope", resource_owner_id: 100 } FactoryBot.create :access_token, app_params empty_scopes = Doorkeeper::OAuth::Scopes.from_string("") last_token = AccessToken.matching_token_for(application, 100, empty_scopes) expect(last_token).to be_nil end it 'matches token if empty scope requested and no token scopes present' do empty_scopes = Doorkeeper::OAuth::Scopes.from_string("") token = FactoryBot.create :access_token, default_attributes.merge(scopes: empty_scopes) last_token = AccessToken.matching_token_for(application, 100, empty_scopes) expect(last_token).to eq(token) end it 'returns the last matching token' do FactoryBot.create :access_token, default_attributes.merge(created_at: 1.day.ago) matching_token = FactoryBot.create :access_token, default_attributes FactoryBot.create :access_token, default_attributes.merge(scopes: 'public') last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to eq(matching_token) end end describe "#as_json" do it "returns as_json hash" do token = FactoryBot.create :access_token token_hash = { resource_owner_id: token.resource_owner_id, scope: token.scopes, expires_in: token.expires_in_seconds, application: { uid: token.application.uid }, created_at: token.created_at.to_i } expect(token.as_json).to eq token_hash end end end end