From: Tom Hughes Date: Thu, 15 Oct 2020 21:07:34 +0000 (+0100) Subject: Add tests for OAuth2 X-Git-Tag: live~2119^2~7 X-Git-Url: https://git.openstreetmap.org./rails.git/commitdiff_plain/f064a18a16de7d5001107116c7916796e0b15a26 Add tests for OAuth2 --- diff --git a/test/controllers/oauth2_applications_controller_test.rb b/test/controllers/oauth2_applications_controller_test.rb new file mode 100644 index 000000000..eec5e02ec --- /dev/null +++ b/test/controllers/oauth2_applications_controller_test.rb @@ -0,0 +1,221 @@ +require "test_helper" + +class Oauth2ApplicationsControllerTest < ActionDispatch::IntegrationTest + ## + # test all routes which lead to this controller + def test_routes + assert_routing( + { :path => "/oauth2/applications", :method => :get }, + { :controller => "oauth2_applications", :action => "index" } + ) + assert_routing( + { :path => "/oauth2/applications", :method => :post }, + { :controller => "oauth2_applications", :action => "create" } + ) + assert_routing( + { :path => "/oauth2/applications/new", :method => :get }, + { :controller => "oauth2_applications", :action => "new" } + ) + assert_routing( + { :path => "/oauth2/applications/1/edit", :method => :get }, + { :controller => "oauth2_applications", :action => "edit", :id => "1" } + ) + assert_routing( + { :path => "/oauth2/applications/1", :method => :get }, + { :controller => "oauth2_applications", :action => "show", :id => "1" } + ) + assert_routing( + { :path => "/oauth2/applications/1", :method => :patch }, + { :controller => "oauth2_applications", :action => "update", :id => "1" } + ) + assert_routing( + { :path => "/oauth2/applications/1", :method => :put }, + { :controller => "oauth2_applications", :action => "update", :id => "1" } + ) + assert_routing( + { :path => "/oauth2/applications/1", :method => :delete }, + { :controller => "oauth2_applications", :action => "destroy", :id => "1" } + ) + end + + def test_index + user = create(:user) + create_list(:oauth_application, 2, :owner => user) + + get oauth_applications_path + assert_response :redirect + assert_redirected_to login_path(:referer => oauth_applications_path) + + session_for(user) + + get oauth_applications_path + assert_response :success + assert_template "oauth2_applications/index" + assert_select "tr", 2 + end + + def test_new + user = create(:user) + + get new_oauth_application_path + assert_response :redirect + assert_redirected_to login_path(:referer => new_oauth_application_path) + + session_for(user) + + get new_oauth_application_path + assert_response :success + assert_template "oauth2_applications/new" + assert_select "form", 1 do + assert_select "input#doorkeeper_application_name", 1 + assert_select "textarea#doorkeeper_application_redirect_uri", 1 + assert_select "input#doorkeeper_application_confidential", 1 + Oauth.scopes.each do |scope| + assert_select "input#doorkeeper_application_scopes_#{scope.name}", 1 + end + end + end + + def test_create + user = create(:user) + + assert_difference "Doorkeeper::Application.count", 0 do + post oauth_applications_path + end + assert_response :forbidden + + session_for(user) + + assert_difference "Doorkeeper::Application.count", 0 do + post oauth_applications_path(:doorkeeper_application => { + :name => "Test Application" + }) + end + assert_response :success + assert_template "oauth2_applications/new" + + assert_difference "Doorkeeper::Application.count", 0 do + post oauth_applications_path(:doorkeeper_application => { + :name => "Test Application", + :redirect_uri => "https://test.example.com/", + :scopes => ["bad_scope"] + }) + end + assert_response :success + assert_template "oauth2_applications/new" + + assert_difference "Doorkeeper::Application.count", 1 do + post oauth_applications_path(:doorkeeper_application => { + :name => "Test Application", + :redirect_uri => "https://test.example.com/", + :scopes => ["read_prefs"] + }) + end + assert_response :redirect + assert_redirected_to oauth_application_path(:id => Doorkeeper::Application.find_by(:name => "Test Application").id) + end + + def test_show + user = create(:user) + client = create(:oauth_application, :owner => user) + other_client = create(:oauth_application) + + get oauth_application_path(:id => client) + assert_response :redirect + assert_redirected_to login_path(:referer => oauth_application_path(:id => client.id)) + + session_for(user) + + get oauth_application_path(:id => other_client) + assert_response :not_found + assert_template "oauth2_applications/not_found" + + get oauth_application_path(:id => client) + assert_response :success + assert_template "oauth2_applications/show" + end + + def test_edit + user = create(:user) + client = create(:oauth_application, :owner => user) + other_client = create(:oauth_application) + + get edit_oauth_application_path(:id => client) + assert_response :redirect + assert_redirected_to login_path(:referer => edit_oauth_application_path(:id => client.id)) + + session_for(user) + + get edit_oauth_application_path(:id => other_client) + assert_response :not_found + assert_template "oauth2_applications/not_found" + + get edit_oauth_application_path(:id => client) + assert_response :success + assert_template "oauth2_applications/edit" + assert_select "form", 1 do + assert_select "input#doorkeeper_application_name", 1 + assert_select "textarea#doorkeeper_application_redirect_uri", 1 + assert_select "input#doorkeeper_application_confidential", 1 + Oauth.scopes.each do |scope| + assert_select "input#doorkeeper_application_scopes_#{scope.name}", 1 + end + end + end + + def test_update + user = create(:user) + client = create(:oauth_application, :owner => user) + other_client = create(:oauth_application) + + put oauth_application_path(:id => client) + assert_response :forbidden + + session_for(user) + + put oauth_application_path(:id => other_client) + assert_response :not_found + assert_template "oauth2_applications/not_found" + + put oauth_application_path(:id => client, + :doorkeeper_application => { + :name => "New Name", + :redirect_uri => nil + }) + assert_response :success + assert_template "oauth2_applications/edit" + + put oauth_application_path(:id => client, + :doorkeeper_application => { + :name => "New Name", + :redirect_uri => "https://new.example.com/url" + }) + assert_response :redirect + assert_redirected_to oauth_application_path(:id => client.id) + end + + def test_destroy + user = create(:user) + client = create(:oauth_application, :owner => user) + other_client = create(:oauth_application) + + assert_difference "Doorkeeper::Application.count", 0 do + delete oauth_application_path(:id => client) + end + assert_response :forbidden + + session_for(user) + + assert_difference "Doorkeeper::Application.count", 0 do + delete oauth_application_path(:id => other_client) + end + assert_response :not_found + assert_template "oauth2_applications/not_found" + + assert_difference "Doorkeeper::Application.count", -1 do + delete oauth_application_path(:id => client) + end + assert_response :redirect + assert_redirected_to oauth_applications_path + end +end diff --git a/test/controllers/oauth2_authorizations_controller_test.rb b/test/controllers/oauth2_authorizations_controller_test.rb new file mode 100644 index 000000000..19bc79808 --- /dev/null +++ b/test/controllers/oauth2_authorizations_controller_test.rb @@ -0,0 +1,184 @@ +require "test_helper" + +class Oauth2AuthorizationsControllerTest < ActionDispatch::IntegrationTest + ## + # test all routes which lead to this controller + def test_routes + assert_routing( + { :path => "/oauth2/authorize", :method => :get }, + { :controller => "oauth2_authorizations", :action => "new" } + ) + assert_routing( + { :path => "/oauth2/authorize", :method => :post }, + { :controller => "oauth2_authorizations", :action => "create" } + ) + assert_routing( + { :path => "/oauth2/authorize", :method => :delete }, + { :controller => "oauth2_authorizations", :action => "destroy" } + ) + assert_routing( + { :path => "/oauth2/authorize/native", :method => :get }, + { :controller => "oauth2_authorizations", :action => "show" } + ) + end + + def test_new + application = create(:oauth_application, :scopes => "write_api") + + get oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api") + assert_response :redirect + assert_redirected_to login_path(:referer => oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api")) + + session_for(create(:user)) + + get oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api") + assert_response :success + assert_template "oauth2_authorizations/new" + end + + def test_new_native + application = create(:oauth_application, :scopes => "write_api", :redirect_uri => "urn:ietf:wg:oauth:2.0:oob") + + get oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api") + assert_response :redirect + assert_redirected_to login_path(:referer => oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api")) + + session_for(create(:user)) + + get oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api") + assert_response :success + assert_template "oauth2_authorizations/new" + end + + def test_new_bad_uri + application = create(:oauth_application, :scopes => "write_api") + + session_for(create(:user)) + + get oauth_authorization_path(:client_id => application.uid, + :redirect_uri => "https://bad.example.com/", + :response_type => "code", + :scope => "write_api") + assert_response :success + assert_template "oauth2_authorizations/error" + assert_select "p", "The requested redirect uri is malformed or doesn't match client redirect URI." + end + + def test_new_bad_scope + application = create(:oauth_application, :scopes => "write_api") + + session_for(create(:user)) + + get oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "bad_scope") + assert_response :success + assert_template "oauth2_authorizations/error" + assert_select "p", "The requested scope is invalid, unknown, or malformed." + + get oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_prefs") + assert_response :success + assert_template "oauth2_authorizations/error" + assert_select "p", "The requested scope is invalid, unknown, or malformed." + end + + def test_create + application = create(:oauth_application, :scopes => "write_api") + + post oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api") + assert_response :forbidden + + session_for(create(:user)) + + post oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api") + assert_response :redirect + assert_redirected_to(/^#{Regexp.escape(application.redirect_uri)}\?code=/) + end + + def test_create_native + application = create(:oauth_application, :scopes => "write_api", :redirect_uri => "urn:ietf:wg:oauth:2.0:oob") + + post oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api") + assert_response :forbidden + + session_for(create(:user)) + + post oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api") + assert_response :redirect + assert_equal native_oauth_authorization_path, URI.parse(response.location).path + follow_redirect! + assert_response :success + assert_template "oauth2_authorizations/show" + end + + def test_destroy + application = create(:oauth_application) + + delete oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api") + assert_response :forbidden + + session_for(create(:user)) + + delete oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api") + assert_response :redirect + assert_redirected_to(/^#{Regexp.escape(application.redirect_uri)}\?error=access_denied/) + end + + def test_destroy_native + application = create(:oauth_application, :redirect_uri => "urn:ietf:wg:oauth:2.0:oob") + + delete oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api") + assert_response :forbidden + + session_for(create(:user)) + + delete oauth_authorization_path(:client_id => application.uid, + :redirect_uri => application.redirect_uri, + :response_type => "code", + :scope => "write_api") + assert_response :bad_request + end +end diff --git a/test/controllers/oauth2_authorized_applications_controller_test.rb b/test/controllers/oauth2_authorized_applications_controller_test.rb new file mode 100644 index 000000000..45a60efcb --- /dev/null +++ b/test/controllers/oauth2_authorized_applications_controller_test.rb @@ -0,0 +1,63 @@ +require "test_helper" + +class Oauth2AuthorizedApplicationsControllerTest < ActionDispatch::IntegrationTest + ## + # test all routes which lead to this controller + def test_routes + assert_routing( + { :path => "/oauth2/authorized_applications", :method => :get }, + { :controller => "oauth2_authorized_applications", :action => "index" } + ) + assert_routing( + { :path => "/oauth2/authorized_applications/1", :method => :delete }, + { :controller => "oauth2_authorized_applications", :action => "destroy", :id => "1" } + ) + end + + def test_index + user = create(:user) + application1 = create(:oauth_application) + create(:oauth_access_grant, :resource_owner_id => user.id, :application => application1) + create(:oauth_access_token, :resource_owner_id => user.id, :application => application1) + application2 = create(:oauth_application) + create(:oauth_access_grant, :resource_owner_id => user.id, :application => application2) + create(:oauth_access_token, :resource_owner_id => user.id, :application => application2) + create(:oauth_application) + + get oauth_authorized_applications_path + assert_response :redirect + assert_redirected_to login_path(:referer => oauth_authorized_applications_path) + + session_for(user) + + get oauth_authorized_applications_path + assert_response :success + assert_template "oauth2_authorized_applications/index" + assert_select "tr", 2 + end + + def test_destroy + user = create(:user) + application1 = create(:oauth_application) + create(:oauth_access_grant, :resource_owner_id => user.id, :application => application1) + create(:oauth_access_token, :resource_owner_id => user.id, :application => application1) + application2 = create(:oauth_application) + create(:oauth_access_grant, :resource_owner_id => user.id, :application => application2) + create(:oauth_access_token, :resource_owner_id => user.id, :application => application2) + create(:oauth_application) + + delete oauth_authorized_application_path(:id => application1.id) + assert_response :forbidden + + session_for(user) + + delete oauth_authorized_application_path(:id => application1.id) + assert_response :redirect + assert_redirected_to oauth_authorized_applications_path + + get oauth_authorized_applications_path + assert_response :success + assert_template "oauth2_authorized_applications/index" + assert_select "tr", 1 + end +end diff --git a/test/factories/oauth_access_grant.rb b/test/factories/oauth_access_grant.rb new file mode 100644 index 000000000..caddea815 --- /dev/null +++ b/test/factories/oauth_access_grant.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :oauth_access_grant, :class => "Doorkeeper::AccessGrant" do + association :resource_owner_id, :factory => :user + association :application, :factory => :oauth_application + + expires_in { 86400 } + redirect_uri { application.redirect_uri } + end +end diff --git a/test/factories/oauth_access_token.rb b/test/factories/oauth_access_token.rb new file mode 100644 index 000000000..c0f624530 --- /dev/null +++ b/test/factories/oauth_access_token.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :oauth_access_token, :class => "Doorkeeper::AccessToken" do + association :resource_owner_id, :factory => :user + association :application, :factory => :oauth_application + end +end diff --git a/test/factories/oauth_applications.rb b/test/factories/oauth_applications.rb new file mode 100644 index 000000000..a9b3b875d --- /dev/null +++ b/test/factories/oauth_applications.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :oauth_application, :class => "Doorkeeper::Application" do + sequence(:name) { |n| "OAuth application #{n}" } + sequence(:redirect_uri) { |n| "https://example.com/app/#{n}" } + + association :owner, :factory => :user + end +end diff --git a/test/integration/oauth2_test.rb b/test/integration/oauth2_test.rb new file mode 100644 index 000000000..8de381c65 --- /dev/null +++ b/test/integration/oauth2_test.rb @@ -0,0 +1,170 @@ +require "test_helper" + +class OAuth2Test < ActionDispatch::IntegrationTest + def test_oauth2 + client = create(:oauth_application, :redirect_uri => "https://some.web.app.example.org/callback", :scopes => "read_prefs write_api read_gpx") + state = SecureRandom.urlsafe_base64(16) + + authorize_client(client, :state => state) + assert_response :redirect + code = validate_redirect(client, state) + + token = request_token(client, code) + + test_token(token, client) + end + + def test_oauth2_oob + client = create(:oauth_application, :redirect_uri => "urn:ietf:wg:oauth:2.0:oob", :scopes => "read_prefs write_api read_gpx") + + authorize_client(client) + assert_response :redirect + follow_redirect! + assert_response :success + assert_template "oauth2_authorizations/show" + m = response.body.match(%r{([A-Za-z0-9_-]+)}) + assert_not_nil m + code = m[1] + + token = request_token(client, code) + + test_token(token, client) + end + + def test_oauth2_pkce_plain + client = create(:oauth_application, :redirect_uri => "https://some.web.app.example.org/callback", :scopes => "read_prefs write_api read_gpx") + state = SecureRandom.urlsafe_base64(16) + verifier = SecureRandom.urlsafe_base64(48) + challenge = verifier + + authorize_client(client, :state => state, :code_challenge => challenge, :code_challenge_method => "plain") + assert_response :redirect + code = validate_redirect(client, state) + + token = request_token(client, code, verifier) + + test_token(token, client) + end + + def test_oauth2_pkce_s256 + client = create(:oauth_application, :redirect_uri => "https://some.web.app.example.org/callback", :scopes => "read_prefs write_api read_gpx") + state = SecureRandom.urlsafe_base64(16) + verifier = SecureRandom.urlsafe_base64(48) + challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), :padding => false) + + authorize_client(client, :state => state, :code_challenge => challenge, :code_challenge_method => "S256") + assert_response :redirect + code = validate_redirect(client, state) + + token = request_token(client, code, verifier) + + test_token(token, client) + end + + private + + def authorize_client(client, options = {}) + options = options.merge(:client_id => client.uid, + :redirect_uri => client.redirect_uri, + :response_type => "code", + :scope => "read_prefs") + + get oauth_authorization_path(options) + assert_response :redirect + assert_redirected_to login_path(:referer => request.fullpath) + + user = create(:user) + + post login_path(:username => user.email, :password => "test") + follow_redirect! + assert_response :success + + get oauth_authorization_path(options) + assert_response :success + assert_template "oauth2_authorizations/new" + + delete oauth_authorization_path(options) + + validate_deny(client, options) + + post oauth_authorization_path(options) + end + + def validate_deny(client, options) + if client.redirect_uri == "urn:ietf:wg:oauth:2.0:oob" + assert_response :bad_request + else + assert_response :redirect + location = URI.parse(response.location) + assert_match(/^#{Regexp.escape(client.redirect_uri)}/, location.to_s) + query = Rack::Utils.parse_query(location.query) + assert_equal "access_denied", query["error"] + assert_equal "The resource owner or authorization server denied the request.", query["error_description"] + assert_equal options[:state], query["state"] + end + end + + def validate_redirect(client, state) + location = URI.parse(response.location) + assert_match(/^#{Regexp.escape(client.redirect_uri)}/, location.to_s) + query = Rack::Utils.parse_query(location.query) + assert_equal state, query["state"] + + query["code"] + end + + def request_token(client, code, verifier = nil) + options = { + :client_id => client.uid, + :client_secret => client.plaintext_secret, + :code => code, + :grant_type => "authorization_code", + :redirect_uri => client.redirect_uri + } + + if verifier + post oauth_token_path(options) + assert_response :bad_request + + options = options.merge(:code_verifier => verifier) + end + + post oauth_token_path(options) + assert_response :success + token = JSON.parse(response.body) + assert_equal "Bearer", token["token_type"] + assert_equal "read_prefs", token["scope"] + + token["access_token"] + end + + def test_token(token, client) + get user_preferences_path + assert_response :unauthorized + + auth_header = bearer_authorization_header(token) + + get user_preferences_path, :headers => auth_header + assert_response :success + + get user_preferences_path(:access_token => token) + assert_response :unauthorized + + get user_preferences_path(:bearer_token => token) + assert_response :unauthorized + + get api_trace_path(:id => 2), :headers => auth_header + assert_response :forbidden + + post oauth_revoke_path(:token => token) + assert_response :forbidden + + post oauth_revoke_path(:token => token, + :client_id => client.uid, + :client_secret => client.plaintext_secret) + assert_response :success + + get user_preferences_path, :headers => auth_header + assert_response :unauthorized + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index a6147ef29..505fa2568 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -138,6 +138,12 @@ module ActiveSupport { "Authorization" => format("Basic %s", :auth => Base64.encode64("#{user}:#{pass}")) } end + ## + # return request header for HTTP Bearer Authorization + def bearer_authorization_header(token) + { "Authorization" => "Bearer #{token}" } + end + ## # make an OAuth signed request def signed_request(method, uri, options = {})