require 'rails_helper' RSpec.describe RsOauthManager::ExchangeCode do let(:user) { create :user } # Example values from RFC 7636, Appendix B let(:code_verifier) { "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" } let(:code_challenge) { "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" } before do allow_any_instance_of(AppCatalog::WebApp).to receive(:update_metadata).and_return(true) allow_any_instance_of(RemoteStorageAuthorization).to receive(:remove_token_expiry_job).and_return(nil) end def issue_code(**overrides) RsOauthManager::IssueCode.call( user: user, client_id: "example.com", redirect_uri: "https://example.com", permissions: %w(documents:rw), code_challenge: code_challenge, code_challenge_method: "S256", user_agent: "RSpec Device", **overrides ) end def exchange(code, **overrides) described_class.call( code: code.code, client_id: "https://example.com", redirect_uri: "https://example.com", code_verifier: code_verifier, **overrides ) end it "creates an authorization and consumes the code" do code = issue_code auth = nil expect { auth = exchange(code) }.to change(RemoteStorageAuthorization, :count).by(1) expect(auth.permissions).to eq(%w(documents:rw)) expect(auth.client_id).to eq("example.com") expect(auth.token).to be_present expect(auth.user_agent).to eq("RSpec Device") expect(RsAuthorizationCode.exists?(code.id)).to be(false) end it "carries over the permission expiry" do expire_at = 1.month.from_now code = issue_code(expire_at: expire_at) auth = exchange(code) expect(auth.expire_at).to be_within(1.second).of(expire_at) end it "rejects an unknown code" do expect do described_class.call( code: "unknown", client_id: "https://example.com", redirect_uri: "https://example.com", code_verifier: code_verifier ) end.to raise_error(RsOauthManagerService::InvalidGrant) end it "rejects a reused code" do code = issue_code exchange(code) expect { exchange(code) }.to raise_error(RsOauthManagerService::InvalidGrant) end it "rejects an expired code" do code = issue_code code.update_columns(expires_at: 1.minute.ago) expect { exchange(code) }.to raise_error(RsOauthManagerService::InvalidGrant) end it "rejects a mismatching redirect_uri" do code = issue_code expect { exchange(code, redirect_uri: "https://example.com/other") } .to raise_error(RsOauthManagerService::InvalidGrant) end it "rejects a mismatching client_id host" do code = issue_code expect { exchange(code, client_id: "https://evil.example.org") } .to raise_error(RsOauthManagerService::InvalidGrant) end it "rejects a bad code_verifier" do code = issue_code expect { exchange(code, code_verifier: "wrong-verifier") } .to raise_error(RsOauthManagerService::InvalidGrant) end end