Support multiple redirect_uris when creating OAuth 2.0 Applications (#29192)
This commit is contained in:
		
							parent
							
								
									12472e7f40
								
							
						
					
					
						commit
						2da2a1dae9
					
				| @ -4,6 +4,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController | |||||||
|   def show |   def show | ||||||
|     return doorkeeper_render_error unless valid_doorkeeper_token? |     return doorkeeper_render_error unless valid_doorkeeper_token? | ||||||
| 
 | 
 | ||||||
|     render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key client_id scopes) |     render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ class Api::V1::AppsController < Api::BaseController | |||||||
| 
 | 
 | ||||||
|   def create |   def create | ||||||
|     @app = Doorkeeper::Application.create!(application_options) |     @app = Doorkeeper::Application.create!(application_options) | ||||||
|     render json: @app, serializer: REST::ApplicationSerializer |     render json: @app, serializer: REST::CredentialApplicationSerializer | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
| @ -24,6 +24,6 @@ class Api::V1::AppsController < Api::BaseController | |||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def app_params |   def app_params | ||||||
|     params.permit(:client_name, :redirect_uris, :scopes, :website) |     params.permit(:client_name, :scopes, :website, :redirect_uris, redirect_uris: []) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -23,6 +23,12 @@ module ApplicationExtension | |||||||
|     redirect_uri.lines.first.strip |     redirect_uri.lines.first.strip | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def redirect_uris | ||||||
|  |     # Doorkeeper stores the redirect_uri value as a newline delimeted list in | ||||||
|  |     # the database: | ||||||
|  |     redirect_uri.split | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def push_to_streaming_api |   def push_to_streaming_api | ||||||
|     # TODO: #28793 Combine into a single topic |     # TODO: #28793 Combine into a single topic | ||||||
|     payload = Oj.dump(event: :kill) |     payload = Oj.dump(event: :kill) | ||||||
|  | |||||||
| @ -1,24 +1,18 @@ | |||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class REST::ApplicationSerializer < ActiveModel::Serializer | class REST::ApplicationSerializer < ActiveModel::Serializer | ||||||
|   attributes :id, :name, :website, :scopes, :redirect_uri, |   attributes :id, :name, :website, :scopes, :redirect_uris | ||||||
|              :client_id, :client_secret |  | ||||||
| 
 | 
 | ||||||
|   # NOTE: Deprecated in 4.3.0, needs to be removed in 5.0.0 |   # NOTE: Deprecated in 4.3.0, needs to be removed in 5.0.0 | ||||||
|   attribute :vapid_key |   attribute :vapid_key | ||||||
| 
 | 
 | ||||||
|  |   # We should consider this property deprecated for 4.3.0 | ||||||
|  |   attribute :redirect_uri | ||||||
|  | 
 | ||||||
|   def id |   def id | ||||||
|     object.id.to_s |     object.id.to_s | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def client_id |  | ||||||
|     object.uid |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def client_secret |  | ||||||
|     object.secret |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def website |   def website | ||||||
|     object.website.presence |     object.website.presence | ||||||
|   end |   end | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								app/serializers/rest/credential_application_serializer.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/serializers/rest/credential_application_serializer.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class REST::CredentialApplicationSerializer < REST::ApplicationSerializer | ||||||
|  |   attributes :client_id, :client_secret | ||||||
|  | 
 | ||||||
|  |   def client_id | ||||||
|  |     object.uid | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def client_secret | ||||||
|  |     object.secret | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -20,14 +20,26 @@ describe 'Credentials' do | |||||||
| 
 | 
 | ||||||
|         expect(body_as_json).to match( |         expect(body_as_json).to match( | ||||||
|           a_hash_including( |           a_hash_including( | ||||||
|  |             id: token.application.id.to_s, | ||||||
|             name: token.application.name, |             name: token.application.name, | ||||||
|             website: token.application.website, |             website: token.application.website, | ||||||
|             vapid_key: Rails.configuration.x.vapid_public_key, |  | ||||||
|             scopes: token.application.scopes.map(&:to_s), |             scopes: token.application.scopes.map(&:to_s), | ||||||
|             client_id: token.application.uid |             redirect_uris: token.application.redirect_uris, | ||||||
|  |             # Deprecated properties as of 4.3: | ||||||
|  |             redirect_uri: token.application.redirect_uri.split.first, | ||||||
|  |             vapid_key: Rails.configuration.x.vapid_public_key | ||||||
|           ) |           ) | ||||||
|         ) |         ) | ||||||
|       end |       end | ||||||
|  | 
 | ||||||
|  |       it 'does not expose the client_id or client_secret' do | ||||||
|  |         subject | ||||||
|  | 
 | ||||||
|  |         expect(response).to have_http_status(200) | ||||||
|  | 
 | ||||||
|  |         expect(body_as_json[:client_id]).to_not be_present | ||||||
|  |         expect(body_as_json[:client_secret]).to_not be_present | ||||||
|  |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'with a non-read scoped oauth token' do |     context 'with a non-read scoped oauth token' do | ||||||
| @ -46,11 +58,14 @@ describe 'Credentials' do | |||||||
| 
 | 
 | ||||||
|         expect(body_as_json).to match( |         expect(body_as_json).to match( | ||||||
|           a_hash_including( |           a_hash_including( | ||||||
|  |             id: token.application.id.to_s, | ||||||
|             name: token.application.name, |             name: token.application.name, | ||||||
|             website: token.application.website, |             website: token.application.website, | ||||||
|             vapid_key: Rails.configuration.x.vapid_public_key, |  | ||||||
|             scopes: token.application.scopes.map(&:to_s), |             scopes: token.application.scopes.map(&:to_s), | ||||||
|             client_id: token.application.uid |             redirect_uris: token.application.redirect_uris, | ||||||
|  |             # Deprecated properties as of 4.3: | ||||||
|  |             redirect_uri: token.application.redirect_uri.split.first, | ||||||
|  |             vapid_key: Rails.configuration.x.vapid_public_key | ||||||
|           ) |           ) | ||||||
|         ) |         ) | ||||||
|       end |       end | ||||||
|  | |||||||
| @ -9,8 +9,9 @@ RSpec.describe 'Apps' do | |||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     let(:client_name)   { 'Test app' } |     let(:client_name)   { 'Test app' } | ||||||
|     let(:scopes)        { nil } |     let(:scopes)        { 'read write' } | ||||||
|     let(:redirect_uris) { 'urn:ietf:wg:oauth:2.0:oob' } |     let(:redirect_uri)  { 'urn:ietf:wg:oauth:2.0:oob' } | ||||||
|  |     let(:redirect_uris) { [redirect_uri] } | ||||||
|     let(:website)       { nil } |     let(:website)       { nil } | ||||||
| 
 | 
 | ||||||
|     let(:params) do |     let(:params) do | ||||||
| @ -26,13 +27,63 @@ RSpec.describe 'Apps' do | |||||||
|       it 'creates an OAuth app', :aggregate_failures do |       it 'creates an OAuth app', :aggregate_failures do | ||||||
|         subject |         subject | ||||||
| 
 | 
 | ||||||
|  |         expect(response).to have_http_status(200) | ||||||
|  | 
 | ||||||
|  |         app = Doorkeeper::Application.find_by(name: client_name) | ||||||
|  | 
 | ||||||
|  |         expect(app).to be_present | ||||||
|  |         expect(app.scopes.to_s).to eq scopes | ||||||
|  |         expect(app.redirect_uris).to eq redirect_uris | ||||||
|  | 
 | ||||||
|  |         expect(body_as_json).to match( | ||||||
|  |           a_hash_including( | ||||||
|  |             id: app.id.to_s, | ||||||
|  |             client_id: app.uid, | ||||||
|  |             client_secret: app.secret, | ||||||
|  |             name: client_name, | ||||||
|  |             website: website, | ||||||
|  |             scopes: ['read', 'write'], | ||||||
|  |             redirect_uris: redirect_uris, | ||||||
|  |             # Deprecated properties as of 4.3: | ||||||
|  |             redirect_uri: redirect_uri, | ||||||
|  |             vapid_key: Rails.configuration.x.vapid_public_key | ||||||
|  |           ) | ||||||
|  |         ) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'without scopes being supplied' do | ||||||
|  |       let(:scopes) { nil } | ||||||
|  | 
 | ||||||
|  |       it 'creates an OAuth App with the default scope' do | ||||||
|  |         subject | ||||||
|  | 
 | ||||||
|         expect(response).to have_http_status(200) |         expect(response).to have_http_status(200) | ||||||
|         expect(Doorkeeper::Application.find_by(name: client_name)).to be_present |         expect(Doorkeeper::Application.find_by(name: client_name)).to be_present | ||||||
| 
 | 
 | ||||||
|         body = body_as_json |         body = body_as_json | ||||||
| 
 | 
 | ||||||
|         expect(body[:client_id]).to be_present |         expect(body[:scopes]).to eq Doorkeeper.config.default_scopes.to_a | ||||||
|         expect(body[:client_secret]).to be_present |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     # FIXME: This is a bug: https://github.com/mastodon/mastodon/issues/30152 | ||||||
|  |     context 'with scopes as an array' do | ||||||
|  |       let(:scopes) { %w(read write follow) } | ||||||
|  | 
 | ||||||
|  |       it 'creates an OAuth App with the default scope' do | ||||||
|  |         subject | ||||||
|  | 
 | ||||||
|  |         expect(response).to have_http_status(200) | ||||||
|  | 
 | ||||||
|  |         app = Doorkeeper::Application.find_by(name: client_name) | ||||||
|  | 
 | ||||||
|  |         expect(app).to be_present | ||||||
|  |         expect(app.scopes.to_s).to eq 'read' | ||||||
|  | 
 | ||||||
|  |         body = body_as_json | ||||||
|  | 
 | ||||||
|  |         expect(body[:scopes]).to eq ['read'] | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
| @ -77,8 +128,8 @@ RSpec.describe 'Apps' do | |||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'with a too-long redirect_uris' do |     context 'with a too-long redirect_uri' do | ||||||
|       let(:redirect_uris) { "https://foo.bar/#{'hoge' * 2_000}" } |       let(:redirect_uris) { "https://app.example/#{'hoge' * 2_000}" } | ||||||
| 
 | 
 | ||||||
|       it 'returns http unprocessable entity' do |       it 'returns http unprocessable entity' do | ||||||
|         subject |         subject | ||||||
| @ -87,8 +138,80 @@ RSpec.describe 'Apps' do | |||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'without required params' do |     # NOTE: This spec currently tests the same as the "with a too-long redirect_uri test case" | ||||||
|       let(:client_name)   { '' } |     context 'with too many redirect_uris' do | ||||||
|  |       let(:redirect_uris) { (0...500).map { |i| "https://app.example/#{i}/callback" } } | ||||||
|  | 
 | ||||||
|  |       it 'returns http unprocessable entity' do | ||||||
|  |         subject | ||||||
|  | 
 | ||||||
|  |         expect(response).to have_http_status(422) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with multiple redirect_uris as a string' do | ||||||
|  |       let(:redirect_uris) { "https://redirect1.example/\napp://redirect2.example/" } | ||||||
|  | 
 | ||||||
|  |       it 'creates an OAuth application with multiple redirect URIs' do | ||||||
|  |         subject | ||||||
|  | 
 | ||||||
|  |         expect(response).to have_http_status(200) | ||||||
|  | 
 | ||||||
|  |         app = Doorkeeper::Application.find_by(name: client_name) | ||||||
|  | 
 | ||||||
|  |         expect(app).to be_present | ||||||
|  |         expect(app.redirect_uri).to eq redirect_uris | ||||||
|  |         expect(app.redirect_uris).to eq redirect_uris.split | ||||||
|  | 
 | ||||||
|  |         body = body_as_json | ||||||
|  | 
 | ||||||
|  |         expect(body[:redirect_uri]).to eq redirect_uris | ||||||
|  |         expect(body[:redirect_uris]).to eq redirect_uris.split | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with multiple redirect_uris as an array' do | ||||||
|  |       let(:redirect_uris) { ['https://redirect1.example/', 'app://redirect2.example/'] } | ||||||
|  | 
 | ||||||
|  |       it 'creates an OAuth application with multiple redirect URIs' do | ||||||
|  |         subject | ||||||
|  | 
 | ||||||
|  |         expect(response).to have_http_status(200) | ||||||
|  | 
 | ||||||
|  |         app = Doorkeeper::Application.find_by(name: client_name) | ||||||
|  | 
 | ||||||
|  |         expect(app).to be_present | ||||||
|  |         expect(app.redirect_uri).to eq redirect_uris.join "\n" | ||||||
|  |         expect(app.redirect_uris).to eq redirect_uris | ||||||
|  | 
 | ||||||
|  |         body = body_as_json | ||||||
|  | 
 | ||||||
|  |         expect(body[:redirect_uri]).to eq redirect_uris.join "\n" | ||||||
|  |         expect(body[:redirect_uris]).to eq redirect_uris | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with an empty redirect_uris array' do | ||||||
|  |       let(:redirect_uris) { [] } | ||||||
|  | 
 | ||||||
|  |       it 'returns http unprocessable entity' do | ||||||
|  |         subject | ||||||
|  | 
 | ||||||
|  |         expect(response).to have_http_status(422) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with just a newline as the redirect_uris string' do | ||||||
|  |       let(:redirect_uris) { "\n" } | ||||||
|  | 
 | ||||||
|  |       it 'returns http unprocessable entity' do | ||||||
|  |         subject | ||||||
|  | 
 | ||||||
|  |         expect(response).to have_http_status(422) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with an empty redirect_uris string' do | ||||||
|       let(:redirect_uris) { '' } |       let(:redirect_uris) { '' } | ||||||
| 
 | 
 | ||||||
|       it 'returns http unprocessable entity' do |       it 'returns http unprocessable entity' do | ||||||
| @ -97,5 +220,30 @@ RSpec.describe 'Apps' do | |||||||
|         expect(response).to have_http_status(422) |         expect(response).to have_http_status(422) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     context 'without a required param' do | ||||||
|  |       let(:client_name) { '' } | ||||||
|  | 
 | ||||||
|  |       it 'returns http unprocessable entity' do | ||||||
|  |         subject | ||||||
|  | 
 | ||||||
|  |         expect(response).to have_http_status(422) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with a website' do | ||||||
|  |       let(:website) { 'https://app.example/' } | ||||||
|  | 
 | ||||||
|  |       it 'creates an OAuth application with the website specified' do | ||||||
|  |         subject | ||||||
|  | 
 | ||||||
|  |         expect(response).to have_http_status(200) | ||||||
|  | 
 | ||||||
|  |         app = Doorkeeper::Application.find_by(name: client_name) | ||||||
|  | 
 | ||||||
|  |         expect(app).to be_present | ||||||
|  |         expect(app.website).to eq website | ||||||
|  |       end | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user