diff --git a/Changelog.md b/Changelog.md
index 77376c5d0a1644b7bd49ecbcd80633342d96dc7a..e82e1dacde637077e039491d8e5c5b6b87f917ed 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -3,12 +3,14 @@
 **Attention:** This release includes a potentially long running migration! However it should be safe to run this while keeping your application servers on.
 
 ## Refactor
+* Service and ServiceController, general code reorg to make it cleaner/+ testable/+ extensible
 * Background actual mailing when sending invitations [#4069](https://github.com/diaspora/diaspora/issues/4069)
 * Set the current user on the client side through gon [#4028](https://github.com/diaspora/diaspora/issues/4028)
 * Update sign out route to a DELETE request [#4068](https://github.com/diaspora/diaspora/issues/4068)
 * Convert all ActivityStreams::Photo to StatusMessages and drop ActivityStreams::Photo [#4144](https://github.com/diaspora/diaspora/issues/4144)
 
 ## Bug fixes
+* Check twitter write access before adding/authorizing it for a user.
 * Don't focus comment form on 'show n more comments' [#4265](https://github.com/diaspora/diaspora/issues/4265)
 * Do not render mobile photo view for none-existing photos [#4194](https://github.com/diaspora/diaspora/issues/4194)
 * Render markdown content for prettier email subjects and titles [#4182](https://github.com/diaspora/diaspora/issues/4182)
diff --git a/app/controllers/services_controller.rb b/app/controllers/services_controller.rb
index 6ded9b740e0476c88261347dca5bbd1d279a60a1..b6b3e191d66ce9ceecee859f44e7e58e7f39fc4c 100644
--- a/app/controllers/services_controller.rb
+++ b/app/controllers/services_controller.rb
@@ -7,8 +7,8 @@ class ServicesController < ApplicationController
   # See https://github.com/intridea/omniauth/issues/203
   # See also http://www.communityguides.eu/articles/16
   skip_before_filter :verify_authenticity_token, :only => :create
-
   before_filter :authenticate_user!
+  before_filter :abort_if_already_authorized, :abort_if_read_only_access, :only => :create
 
   respond_to :html
   respond_to :json, :only => :inviter
@@ -17,43 +17,19 @@ class ServicesController < ApplicationController
     @services = current_user.services
   end
 
-  def create
-    auth = request.env['omniauth.auth']
-
-    toke = auth['credentials']['token']
-    secret = auth['credentials']['secret']
-
-    provider = auth['provider']
-    user     = auth['info']
-
-    service = "Services::#{provider.camelize}".constantize.new(:nickname => user['nickname'],
-                                                               :access_token => toke,
-                                                               :access_secret => secret,
-                                                               :uid => auth['uid'])
-    current_user.services << service
-
-    if service.persisted?
-      fetch_photo = current_user.profile[:image_url].blank?
+  def create 
+    service = Service.initialize_from_omniauth( omniauth_hash )
+    
+    if current_user.services << service
+      current_user.update_profile_with_omniauth( service.info )
 
-      current_user.update_profile(current_user.profile.from_omniauth_hash(user))
-      Workers::FetchProfilePhoto.perform_async(current_user.id, service.id, user["image"]) if fetch_photo
+      fetch_photo(service) if no_profile_image?
 
       flash[:notice] = I18n.t 'services.create.success'
     else
       flash[:error] = I18n.t 'services.create.failure'
-
-      if existing_service = Service.where(:type => service.type.to_s, :uid => service.uid).first
-        flash[:error] <<  I18n.t('services.create.already_authorized',
-                                 :diaspora_id => existing_service.user.profile.diaspora_handle,
-                                 :service_name => provider.camelize )
-      end
-    end
-    
-    if request.env['omniauth.origin'].nil?
-      render :text => ("<script>window.close()</script>")
-    else
-      redirect_to request.env['omniauth.origin']
     end
+    redirect_to_origin
   end
 
   def failure
@@ -68,4 +44,50 @@ class ServicesController < ApplicationController
     flash[:notice] = I18n.t 'services.destroy.success'
     redirect_to services_url
   end
+
+  private
+
+  def abort_if_already_authorized
+    if service = Service.where(uid: omniauth_hash['uid']).first
+      flash[:error] =  I18n.t( 'services.create.already_authorized',
+                                  diaspora_id:  service.user.profile.diaspora_handle,
+                                  service_name: service.provider.camelize )
+      redirect_to_origin
+    end
+  end
+
+  def abort_if_read_only_access
+    if header_hash["x_access_level"] && header_hash["x_access_level"] == 'read'
+      flash[:error] =  I18n.t( 'services.create.read_only_access' )
+      redirect_to_origin
+    end
+  end
+
+  def redirect_to_origin
+    if origin 
+      redirect_to origin
+    else 
+      render(text: "<script>window.close()</script>")
+    end
+  end
+
+  def no_profile_image?
+    current_user.profile[:image_url].blank?
+  end
+
+  def fetch_photo(service)
+    Workers::FetchProfilePhoto.perform_async(current_user.id, service.id, service.info["image"])
+  end
+
+  def origin
+    request.env['omniauth.origin']
+  end
+
+  def omniauth_hash 
+    request.env['omniauth.auth']
+  end
+
+  def header_hash 
+    omniauth_hash['extra'] ? omniauth_hash['extra']['access_token']['response']['header'] : {}
+  end 
 end
diff --git a/app/models/profile.rb b/app/models/profile.rb
index 5a8073d3b965f7d4a37b57b3337afbfb62bc6ac2..e9a9b8cc360548e383cc278fa4e318d0491c6663 100644
--- a/app/models/profile.rb
+++ b/app/models/profile.rb
@@ -85,7 +85,7 @@ class Profile < ActiveRecord::Base
                'location' =>  'location',
                 }
 
-    update_hash = Hash[omniauth_user_hash.map {|k, v| [mappings[k], v] }]
+    update_hash = Hash[ omniauth_user_hash.map {|k, v| [mappings[k], v] } ]
     
     self.attributes.merge(update_hash){|key, old, new| old.blank? ? new : old}
   end
diff --git a/app/models/service.rb b/app/models/service.rb
index fd5a1e21532e6246c4ee87ce1f4b5835ca3d9eea..2620fecf648bc9e9fd8551bc2a74fc3feab3da1d 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -5,14 +5,12 @@
 class Service < ActiveRecord::Base
   include ActionView::Helpers::TextHelper
   include MarkdownifyHelper
+
+  attr_accessor :provider, :info, :access_level
   
   belongs_to :user
   validates_uniqueness_of :uid, :scope => :type
 
-  def self.titles(service_strings)
-    service_strings.map{|s| "Services::#{s.titleize}"}
-  end
-
   def profile_photo_url
     nil
   end
@@ -21,4 +19,46 @@ class Service < ActiveRecord::Base
     #don't do anything (should be overriden by service extensions)
   end
 
+  class << self
+
+    def titles(service_strings)
+      service_strings.map {|s| "Services::#{s.titleize}"}
+    end
+
+    def first_from_omniauth( auth_hash )
+      @@auth = auth_hash 
+      where( type: service_type, uid: options[:uid] ).first
+    end
+
+    def initialize_from_omniauth( auth_hash )
+      @@auth = auth_hash 
+      service_type.constantize.new( options )
+    end
+
+    def auth
+      @@auth
+    end
+
+    def service_type
+      "Services::#{options[:provider].camelize}"
+    end
+
+    def access_level
+      auth['extra']['access_token']['response']['header']['x_access_level'] if auth['extra']
+    end
+
+    def options
+      { 
+        nickname:      auth['info']['nickname'],
+        access_token:  auth['credentials']['token'],
+        access_secret: auth['credentials']['secret'],
+        uid:           auth['uid'],
+        provider:      auth['provider'],
+        info:          auth['info'], 
+        access_level:  access_level  
+      }
+    end
+
+    private :auth, :service_type, :access_level, :options
+  end
 end
diff --git a/app/models/user.rb b/app/models/user.rb
index a53bfe4888b960b6dfc3fa02324cece110108f8a..1d4491097fcdca7f3913d27843c45d8d4f331e16 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -350,6 +350,10 @@ class User < ActiveRecord::Base
     end
   end
 
+  def update_profile_with_omniauth( user_info )
+    update_profile( self.profile.from_omniauth_hash( user_info ) )
+  end 
+
   def deliver_profile_update
     Postzord::Dispatcher.build(self, profile).post
   end
diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml
index ed7e43c6e77ce9e7b0f58acb094a2aa7e54e1320..a24facc0eac06129c3791ffb1d7ed5ccc60d3915 100644
--- a/config/locales/diaspora/en.yml
+++ b/config/locales/diaspora/en.yml
@@ -760,6 +760,7 @@ en:
       success: "Authentication successful."
       failure: "Authentication failed."
       already_authorized: "A user with diaspora id %{diaspora_id} already authorized that %{service_name} account."
+      read_only_access: "Access level is read-only, please try to authorize again later"
     destroy:
       success: "Successfully deleted authentication."
     failure:
diff --git a/spec/controllers/services_controller_spec.rb b/spec/controllers/services_controller_spec.rb
index 3446b1730af863aaefccb8612e27ce8885711de2..70b73e6ec442adf239aed98fc02ae29d1b64b6b3 100644
--- a/spec/controllers/services_controller_spec.rb
+++ b/spec/controllers/services_controller_spec.rb
@@ -5,61 +5,78 @@
 require 'spec_helper'
 
 describe ServicesController do
-  let(:mock_access_token) { Object.new }
-
-  let(:omniauth_auth) {
+  let(:omniauth_auth) do
     { 'provider' => 'twitter',
       'uid'      => '2',
       'info'   => { 'nickname' => 'grimmin' },
-      'credentials' => { 'token' => 'tokin', 'secret' =>"not_so_much" }
-      }
-  }
+      'credentials' => { 'token' => 'tokin', 'secret' =>"not_so_much" }}
+    end
+  let(:user) { alice }
 
   before do
-    @user   = alice
-    sign_in :user, @user
-    @controller.stub!(:current_user).and_return(@user)
-    mock_access_token.stub!(:token => "12345", :secret => "56789")
+    sign_in :user, user
+    @controller.stub!(:current_user).and_return(user)
   end
 
   describe '#index' do
-    it 'displays all connected serivices for a user' do
-      4.times do
-        FactoryGirl.create(:service, :user => @user)
-      end
+    before do
+      FactoryGirl.create(:service, user: user)
+    end
 
+    it "displays user's connected services" do
       get :index
-      assigns[:services].should == @user.services
+      assigns[:services].should == user.services
     end
   end
 
   describe '#create' do
-    context 'when not fetching a photo' do
-      before do
-        request.env['omniauth.auth'] = omniauth_auth
-      end
+    before do
+      request.env['omniauth.auth'] = omniauth_auth
+      request.env['omniauth.origin'] = root_url
+    end
+
+    it 'creates a new service and associates it with the current user' do
+      expect {
+        post :create, :provider => 'twitter'
+      }.to change(user.services, :count).by(1)
+    end
+
+    it 'saves the provider' do
+      post :create, :provider => 'twitter'
+      user.reload.services.first.class.name.should == "Services::Twitter"
+    end
 
-      it 'creates a new OmniauthService' do
+    context 'when service exists with the same uid' do
+      before { Services::Twitter.create!(uid: omniauth_auth['uid'], user_id: user.id) }
+
+      it 'doesnt create a new service' do
         expect {
-          post :create, :provider => 'twitter'
-        }.to change(@user.services, :count).by(1)
+        post :create, :provider => 'twitter'
+      }.to_not change(Service, :count).by(1)
+      end
+
+      it 'flashes an already_authorized error with the diaspora handle for the user'  do
+        post :create, :provider => 'twitter'
+        flash[:error].include?(user.profile.diaspora_handle).should be_true
+        flash[:error].include?( 'already authorized' ).should be_true
+      end
+    end
+
+    context 'when the access-level is read-only' do
+      before do 
+        access_level_hash = { 'extra' => { 'access_token' => { 'response' => { 'header' => { 'x_access_level' => 'read' }}}}}
+        request.env['omniauth.auth'] = omniauth_auth["info"].merge!( access_level_hash )
       end
 
-      it 'creates a twitter service' do
-        Service.delete_all
-        @user.getting_started = false
+      it 'doesnt create a new service' do
+        expect {
         post :create, :provider => 'twitter'
-        @user.reload.services.first.class.name.should == "Services::Twitter"
+      }.to_not change(Service, :count).by(1)
       end
 
-      it 'returns error if the user already a service with that uid' do
-        Services::Twitter.create!(:nickname => omniauth_auth["info"]['nickname'],
-                                  :access_token => omniauth_auth['credentials']['token'],
-                                  :access_secret => omniauth_auth['credentials']['secret'],
-                                  :uid => omniauth_auth['uid'],
-                                  :user => bob)
+      it 'flashes an read-only access error'  do
         post :create, :provider => 'twitter'
-        flash[:error].include?(bob.person.profile.diaspora_handle).should be_true
+        flash[:error].include?( 'Access level is read-only' ).should be_true
       end
     end
 
@@ -72,9 +89,7 @@ describe ServicesController do
       end
 
       it 'does not queue a job if the profile photo is set' do
-        profile = @user.person.profile
-        profile[:image_url] = "/non/default/image.jpg"
-        profile.save
+        @controller.stub!(:no_profile_image?).and_return false
 
         Workers::FetchProfilePhoto.should_not_receive(:perform_async)
 
@@ -82,11 +97,9 @@ describe ServicesController do
       end
 
       it 'queues a job to save user photo if the photo does not exist' do
-        profile = @user.person.profile
-        profile[:image_url] = nil
-        profile.save
+        @controller.stub!(:no_profile_image?).and_return true
 
-        Workers::FetchProfilePhoto.should_receive(:perform_async).with(@user.id, anything(), "https://service.com/fallback_lowres.jpg")
+        Workers::FetchProfilePhoto.should_receive(:perform_async).with(user.id, anything(), "https://service.com/fallback_lowres.jpg")
 
         post :create, :provider => 'twitter'
       end
@@ -95,13 +108,13 @@ describe ServicesController do
 
   describe '#destroy' do
     before do
-      @service1 = FactoryGirl.create(:service, :user => @user)
+      @service1 = FactoryGirl.create(:service, :user => user)
     end
 
     it 'destroys a service selected by id' do
       lambda{
         delete :destroy, :id => @service1.id
-      }.should change(@user.services, :count).by(-1)
+      }.should change(user.services, :count).by(-1)
     end
   end
 end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 49d2a72e014702e48acdffd422adbfa7dbe6f3f9..93607d9234718c975f39c6e61bb59a07fd77bc1f 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -19,7 +19,43 @@ describe Service do
   end
 
   it 'by default has no profile photo url' do
-    Service.new.profile_photo_url.should be_nil
+    expect( described_class.new.profile_photo_url ).to be_nil
+  end
+
+  describe '.titles' do
+    it "converts passed service titles into service constants" do
+      expect( described_class.titles( ['twitter'] ) ).to eql ['Services::Twitter']
+    end
+  end
+
+  describe '.first_from_omniauth' do
+    let(:omniauth) { { 'provider' => 'facebook', 'uid' => '1', 'credentials' => {}, 'info' => {} } }
+    it 'first service by provider and uid' do
+      expect( described_class.first_from_omniauth( omniauth ) ).to eql @service
+    end
+  end
+
+  describe '.initialize_from_omniauth' do
+    let(:omniauth) do
+      { 'provider' => 'facebook',
+        'uid'      => '2',
+        'info'   => { 'nickname' => 'grimmin' },
+        'credentials' => { 'token' => 'tokin', 'secret' =>"not_so_much" },
+        'extra' => { 'access_token' => { 'response' => { 'header' => { 'x_access_level' => 'read' }}}}
+      }
+    end
+    let(:subject) { described_class.initialize_from_omniauth( omniauth ) }
+
+    it 'new service obj of type Services::Facebook' do
+      expect( subject.type ).to eql "Services::Facebook"
+    end
+
+    it 'new service obj with oauth options populated' do
+      expect( subject.uid ).to eql "2"
+      expect( subject.nickname ).to eql "grimmin"
+      expect( subject.access_token ).to eql "tokin"
+      expect( subject.access_secret ).to eql "not_so_much"
+      expect( subject.access_level ).to eql "read"
+    end
   end
-  
 end