require 'rails_helper' RSpec.describe Rs::OauthController, type: :controller do let(:user) { create :user } 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 describe "GET /rs/oauth/:username" do context "when user is signed in" do before do sign_in user end context "when username is different than current user" do let(:other_user) { create :user, id: 23, cn: "jomokenyatta", email: "jomo@hotmail.com" } before do get :new, params: { username: other_user.cn, redirect_uri: "https://example.com", client_id: "example.com", scope: "examples" } end it "logs out the users and repeats the request" do url = new_rs_oauth_url other_user.cn, redirect_uri: "https://example.com", client_id: "example.com", scope: "examples" expect(response).to redirect_to(url) end end context "when no valid token exists" do before do get :new, params: { username: user.cn, redirect_uri: "https://example.com", client_id: "example.com", scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", state: "foobar123" } end it "returns a 200" do expect(response.response_code).to eq(200) end it "sets the instance variables" do expected_scopes = %w(documents photos contacts:rw videos:r tasks/work:r) expect(assigns["user"]).to eq(user) expect(assigns["redirect_uri"]).to eq("https://example.com") expect(assigns["scopes"]).to eq(expected_scopes) expect(assigns["client_id"]).to eq("example.com") expect(assigns["root_access_requested"]).to eq(false) expect(assigns["state"]).to eq("foobar123") expect(assigns["denial_url"]).to eq("https://example.com#error=access_denied&state=foobar123") end context "no redirect_uri" do before do get :new, params: { username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", client_id: "https://example.com" } end it "returns a 400" do expect(response.response_code).to eq(400) end end context "no client_id" do before do get :new, params: { username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com" } end it "redirects with invalid_request error" do expect(response).to redirect_to("https://example.com#error=invalid_request") end end context "different host for client_id and redirect_uri" do before do get :new, params: { username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com/foobar", client_id: "https://google.com" } end it "redirects with invalid_client error" do expect(response).to redirect_to("https://example.com/foobar#error=invalid_client") end end end context "when valid token already exists" do before do @auth = user.remote_storage_authorizations.create!( permissions: %w(documents photos contacts:rw videos:r tasks/work:r), client_id: "example.com", redirect_uri: "https://example.com", expire_at: 1.day.from_now ) end after { @auth.destroy } context "with same host for client_id and redirect_uri" do before do get :new, params: { username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com", client_id: "https://example.com" } end it "does not reuse the existing token (renders the consent dialog)" do expect(response.response_code).to eq(200) end end context "with different host for client_id and redirect_uri" do before do get :new, params: { username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://app.example.com", client_id: "https://example.com" } end it "redirects with invalid_client error" do expect(response).to redirect_to("https://app.example.com#error=invalid_client") end end end context "when response_type is code" do before do get :new, params: { username: user.cn, redirect_uri: "https://example.com", client_id: "example.com", scope: "documents", response_type: "code", code_challenge: "challenge123", code_challenge_method: "S256", state: "foobar123" } end it "renders the consent dialog and exposes the code challenge" do expect(response.response_code).to eq(200) expect(assigns["response_type"]).to eq("code") expect(assigns["code_challenge"]).to eq("challenge123") expect(assigns["code_challenge_method"]).to eq("S256") end it "uses a query-based denial url" do expect(assigns["denial_url"]).to eq("https://example.com?error=access_denied&state=foobar123") end end context "when response_type is code without a code_challenge" do before do get :new, params: { username: user.cn, redirect_uri: "https://example.com", client_id: "example.com", scope: "documents", response_type: "code", state: "foobar123" } end it "redirects with invalid_request using a query param" do expect(response).to redirect_to("https://example.com?error=invalid_request&state=foobar123") end end context "when response_type is code with the plain method" do before do get :new, params: { username: user.cn, redirect_uri: "https://example.com", client_id: "example.com", scope: "documents", response_type: "code", code_challenge: "challenge123", code_challenge_method: "plain" } end it "redirects with invalid_request" do expect(response).to redirect_to("https://example.com?error=invalid_request") end end context "no scope" do before do get :new, params: { username: user.cn, redirect_uri: "https://example.com", client_id: "https://example.com", state: "foobar123" } end it "redirects to the redirect_uri with an error code" do expect(response).to redirect_to("https://example.com#error=invalid_scope&state=foobar123") end end context "empty scope" do before do get :new, params: { username: user.cn, scope: "", redirect_uri: "https://example.com", client_id: "https://example.com", state: "foobar123" } end it "redirects to the redirect_uri with an error code" do expect(response).to redirect_to("https://example.com#error=invalid_scope&state=foobar123") end end end context "when user is not signed in" do it "redirects to the signin page with username pre-filled" do get :new, params: { username: user.cn, scope: "documents,photos", redirect_uri: "https://example.com" } expect(response).to redirect_to(new_user_session_path(cn: user.cn, ou: user.ou)) end context "without a redirect_uri" do it "returns a 400 without storing the return location" do get :new, params: { username: user.cn, scope: "documents,photos", client_id: "https://example.com" } expect(response.response_code).to eq(400) expect(session[:user_return_to]).to be_nil end end end describe "root access" do before do sign_in user end describe "full" do before do get :new, params: { username: user.cn, scope: "*:rw", redirect_uri: "https://example.com", client_id: "example.com" } end it "sets the instance variables" do expect(assigns["scopes"]).to eq([":rw"]) expect(assigns["root_access_requested"]).to be(true) end end describe "read-only" do before do get :new, params: { username: user.cn, scope: "*:r", redirect_uri: "https://example.com", client_id: "example.com" } end it "sets the instance variables" do expect(assigns["scopes"]).to eq([":r"]) expect(assigns["root_access_requested"]).to be(true) end end end end describe "POST /rs/oauth/:username" do context "when user is signed in" do before do sign_in user request.user_agent = "RSpec Device" end after do user.remote_storage_authorizations.destroy_all RsAuthorizationCode.destroy_all end context "when no valid token exists" do before do post :create, params: { user_id: user.id, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com", client_id: "example.com", state: "foobar123", expire_at: 1.day.from_now } @auth = user.reload.remote_storage_authorizations.first end it "creates a new token" do expect(@auth.permissions).to eq(%w(documents photos contacts:rw videos:r tasks/work:r)) end it "stores the device's user agent" do expect(@auth.user_agent).to eq("RSpec Device") end it "redirects to the redirect_uri" do expect(response).to redirect_to("https://example.com#access_token=#{@auth.token}&state=foobar123") end end context "when token is expired" do before do @auth = user.remote_storage_authorizations.create!( permissions: %w(documents), client_id: "example.com", redirect_uri: "https://example.com", expire_at: 1.day.ago, token: nil ) post :create, params: { user_id: user.id, scope: "documents", redirect_uri: "https://example.com", client_id: "example.com", state: "foobar123", expire_at: 1.month.from_now } end it "updates the token" do expect(@auth.reload.token).not_to be_nil end end context "root access with several scopes" do before do post :create, params: { user_id: user.id, scope: "*:rw contacts:r", redirect_uri: "https://example.com", client_id: "example.com", expire_at: 1.month.from_now } @auth = user.reload.remote_storage_authorizations.first end it "removes all scopes except for the root permission" do expect(@auth.permissions).to eq(%w(:rw)) end end context "no redirect_uri" do before do post :create, params: { user_id: user.id, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", client_id: "example.com", expire_at: 1.month.from_now } end it "returns a 400" do expect(response.response_code).to eq(400) end end context "no client_id" do before do post :create, params: { user_id: user.id, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com", expire_at: 1.month.from_now } end it "redirects with invalid_request error" do expect(response).to redirect_to("https://example.com#error=invalid_request") end end context "hostnames of client_id and redirect_uri do not match" do before do post :create, params: { user_id: user.id, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", client_id: "fishing.com", redirect_uri: "https://example.com", expire_at: 1.month.from_now } end it "redirects with invalid_client error" do expect(response).to redirect_to("https://example.com#error=invalid_client") end end context "empty scope" do before do post :create, params: { user_id: user.id, scope: "", redirect_uri: "https://example.com", client_id: "example.com", state: "foobar123", expire_at: 1.month.from_now } end it "redirects to the redirect_uri with an error code" do expect(response).to redirect_to("https://example.com#error=invalid_scope&state=foobar123") end end context "with response_type=code" do before do post :create, params: { user_id: user.id, scope: "documents,[photos], contacts:rw", redirect_uri: "https://example.com", client_id: "example.com", state: "foobar123", response_type: "code", code_challenge: "challenge123", code_challenge_method: "S256", expire_at: 1.month.from_now } @code = RsAuthorizationCode.last end it "creates an authorization code" do expect(@code.permissions).to eq(%w(documents photos contacts:rw)) expect(@code.code_challenge).to eq("challenge123") expect(@code.code_challenge_method).to eq("S256") expect(@code.user_agent).to eq("RSpec Device") end it "does not create a remote storage authorization" do expect(user.reload.remote_storage_authorizations.count).to eq(0) end it "redirects with the code in the query string" do location = response.location expect(location).to match(%r{\Ahttps://example\.com\?code=.+&state=foobar123\z}) raw_code = CGI.parse(URI.parse(location).query)["code"].first expect(RsAuthorizationCode.find_by_code(raw_code)).to eq(@code) end end context "with response_type=code but no code_challenge" do before do post :create, params: { user_id: user.id, scope: "documents", redirect_uri: "https://example.com", client_id: "example.com", state: "foobar123", response_type: "code", expire_at: 1.month.from_now } end it "redirects with invalid_request using a query param" do expect(response).to redirect_to("https://example.com?error=invalid_request&state=foobar123") end it "does not create an authorization code" do expect(RsAuthorizationCode.count).to eq(0) end end context "when the user_id is different from the signed in user" do before do post :create, params: { user_id: user.id, scope: "documents,photos", redirect_uri: "https://example.com", client_id: "example.com", expire_at: 1.month.from_now } end it "returns a 403" do post :create, params: { user_id: "69", scope: "documents,photos", redirect_uri: "https://example.com", client_id: "example.com", expire_at: 1.month.from_now } expect(response.response_code).to eq(403) end end end context "when user is not signed in" do it "redirects to the signin page" do post :create, params: { user_id: user.id, scope: "documents,photos", redirect_uri: "https://example.com", client_id: "example.com", expire_at: 1.month.from_now } expect(response).to redirect_to(new_user_session_path) end end end describe "POST /rs/oauth/token" do # Example values from RFC 7636, Appendix B let(:code_verifier) { "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" } let(:code_challenge) { "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" } def issue_code 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" ) end def token_params(code, **overrides) { grant_type: "authorization_code", code: code.code, client_id: "https://example.com", redirect_uri: "https://example.com", code_verifier: code_verifier }.merge(overrides) end after do user.remote_storage_authorizations.destroy_all RsAuthorizationCode.destroy_all end context "with a valid code" do before do @code = issue_code post :token, params: token_params(@code) end it "returns an access token" do expect(response.response_code).to eq(200) json = JSON.parse(response.body) expect(json["access_token"]).to be_present expect(json["token_type"]).to eq("bearer") expect(json["scope"]).to eq("documents:rw") end it "does not cache the response" do expect(response.headers["Cache-Control"]).to eq("no-store") end it "sets CORS headers" do expect(response.headers["Access-Control-Allow-Origin"]).to eq("*") end end context "with a bad code_verifier" do before do @code = issue_code post :token, params: token_params(@code, code_verifier: "wrong-verifier") end it "returns invalid_grant" do expect(response.response_code).to eq(400) expect(JSON.parse(response.body)["error"]).to eq("invalid_grant") end end context "with an unsupported grant_type" do before { post :token, params: { grant_type: "password" } } it "returns unsupported_grant_type" do expect(response.response_code).to eq(400) expect(JSON.parse(response.body)["error"]).to eq("unsupported_grant_type") end end end end