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 | ||||
|     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 | ||||
|  | ||||
| @ -5,7 +5,7 @@ class Api::V1::AppsController < Api::BaseController | ||||
| 
 | ||||
|   def create | ||||
|     @app = Doorkeeper::Application.create!(application_options) | ||||
|     render json: @app, serializer: REST::ApplicationSerializer | ||||
|     render json: @app, serializer: REST::CredentialApplicationSerializer | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| @ -24,6 +24,6 @@ class Api::V1::AppsController < Api::BaseController | ||||
|   end | ||||
| 
 | ||||
|   def app_params | ||||
|     params.permit(:client_name, :redirect_uris, :scopes, :website) | ||||
|     params.permit(:client_name, :scopes, :website, :redirect_uris, redirect_uris: []) | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -23,6 +23,12 @@ module ApplicationExtension | ||||
|     redirect_uri.lines.first.strip | ||||
|   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 | ||||
|     # TODO: #28793 Combine into a single topic | ||||
|     payload = Oj.dump(event: :kill) | ||||
|  | ||||
| @ -1,24 +1,18 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class REST::ApplicationSerializer < ActiveModel::Serializer | ||||
|   attributes :id, :name, :website, :scopes, :redirect_uri, | ||||
|              :client_id, :client_secret | ||||
|   attributes :id, :name, :website, :scopes, :redirect_uris | ||||
| 
 | ||||
|   # NOTE: Deprecated in 4.3.0, needs to be removed in 5.0.0 | ||||
|   attribute :vapid_key | ||||
| 
 | ||||
|   # We should consider this property deprecated for 4.3.0 | ||||
|   attribute :redirect_uri | ||||
| 
 | ||||
|   def id | ||||
|     object.id.to_s | ||||
|   end | ||||
| 
 | ||||
|   def client_id | ||||
|     object.uid | ||||
|   end | ||||
| 
 | ||||
|   def client_secret | ||||
|     object.secret | ||||
|   end | ||||
| 
 | ||||
|   def website | ||||
|     object.website.presence | ||||
|   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( | ||||
|           a_hash_including( | ||||
|             id: token.application.id.to_s, | ||||
|             name: token.application.name, | ||||
|             website: token.application.website, | ||||
|             vapid_key: Rails.configuration.x.vapid_public_key, | ||||
|             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 | ||||
| 
 | ||||
|       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 | ||||
| 
 | ||||
|     context 'with a non-read scoped oauth token' do | ||||
| @ -46,11 +58,14 @@ describe 'Credentials' do | ||||
| 
 | ||||
|         expect(body_as_json).to match( | ||||
|           a_hash_including( | ||||
|             id: token.application.id.to_s, | ||||
|             name: token.application.name, | ||||
|             website: token.application.website, | ||||
|             vapid_key: Rails.configuration.x.vapid_public_key, | ||||
|             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 | ||||
|  | ||||
| @ -9,8 +9,9 @@ RSpec.describe 'Apps' do | ||||
|     end | ||||
| 
 | ||||
|     let(:client_name)   { 'Test app' } | ||||
|     let(:scopes)        { nil } | ||||
|     let(:redirect_uris) { 'urn:ietf:wg:oauth:2.0:oob' } | ||||
|     let(:scopes)        { 'read write' } | ||||
|     let(:redirect_uri)  { 'urn:ietf:wg:oauth:2.0:oob' } | ||||
|     let(:redirect_uris) { [redirect_uri] } | ||||
|     let(:website)       { nil } | ||||
| 
 | ||||
|     let(:params) do | ||||
| @ -26,13 +27,63 @@ RSpec.describe 'Apps' do | ||||
|       it 'creates an OAuth app', :aggregate_failures 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 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(Doorkeeper::Application.find_by(name: client_name)).to be_present | ||||
| 
 | ||||
|         body = body_as_json | ||||
| 
 | ||||
|         expect(body[:client_id]).to be_present | ||||
|         expect(body[:client_secret]).to be_present | ||||
|         expect(body[:scopes]).to eq Doorkeeper.config.default_scopes.to_a | ||||
|       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 | ||||
| 
 | ||||
| @ -77,8 +128,8 @@ RSpec.describe 'Apps' do | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'with a too-long redirect_uris' do | ||||
|       let(:redirect_uris) { "https://foo.bar/#{'hoge' * 2_000}" } | ||||
|     context 'with a too-long redirect_uri' do | ||||
|       let(:redirect_uris) { "https://app.example/#{'hoge' * 2_000}" } | ||||
| 
 | ||||
|       it 'returns http unprocessable entity' do | ||||
|         subject | ||||
| @ -87,8 +138,80 @@ RSpec.describe 'Apps' do | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'without required params' do | ||||
|       let(:client_name)   { '' } | ||||
|     # NOTE: This spec currently tests the same as the "with a too-long redirect_uri test case" | ||||
|     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) { '' } | ||||
| 
 | ||||
|       it 'returns http unprocessable entity' do | ||||
| @ -97,5 +220,30 @@ RSpec.describe 'Apps' do | ||||
|         expect(response).to have_http_status(422) | ||||
|       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 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user