Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rails API support #992

Merged
merged 1 commit into from
Mar 5, 2018
Merged

Conversation

Borzik
Copy link
Contributor

@Borzik Borzik commented Sep 27, 2017

This was influenced by #893, but I took a bit different approach and extended functionality to other areas. It is still WIP, but I just wanted to share what is done already, and I'll be glad to get some advices.

The main premise for this PR is that usually Rails APIs and frontend apps are running on different domains, so we cannot easily identity users by cookies or something like that (e.g. in our project we use devise_token_auth and authenticate users by tokens).

Switching on api_mode configuration will:

  • disable request forgery protection, since we assume user is not authenticated by cookies - if he is, there's no much sense in turning api_mode on
  • remove helpers since we don't rely on HTML views
  • render JSON with status: redirect and redirect_uri instead of directly redirecting user
  • disable applications and authorized_applications controller (maybe I will update it to support JSON as well and enable them back - not sure if it's needed though)

As a side effects, this PR:

  • makes Doorkeeper::Oauth::PreAuthorization serializable by as_json method which will provide all information needed to show preauthorization page
  • (I need more information on this one from someone more familiar with project) sets redirect_uri on Doorkeeper::Oauth::ErrorResponse - for some reason, it was not set before. If we render ErrorResponse with api_mode, it will render the same status: redirect and redirect_uri with error message

Anyone who will use api_mode, will also need some oauth page (which can be easily implemented as a small SPA and that will be running on the same domain as main frontend application). They can make main frontend app do this job, but I assume it is not worth loading the whole app just to show a single dialogue message with Allow/Deny buttons. It will have to be responsible for:

  • logging users in
  • making pre-auth requests which will return client name, and other information
  • sending POST oauth/authorize
  • redirecting user whenever they get status: redirect to redirect_uri provided in response

Please let me know your thoughts about it.

expect(json[:response_type]).to eq subject.response_type
expect(json[:scope]).to eq subject.scope
expect(json[:client_name]).to eq client_name
expect(json[:status]).to eq 'preauthorization'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

end
let(:json){ subject.as_json({}) }
it { is_expected.to respond_to :as_json }
it 'returns correct values' do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

allow(client).to receive(:uid).and_return client_id
allow(client).to receive(:name).and_return client_name
end
let(:json){ subject.as_json({}) }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing to the left of {.


describe "as_json" do
let(:client_id){ 'client_uid_123' }
let(:client_name){ 'Acme Co.' }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing to the left of {.
Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

@@ -151,5 +151,25 @@ module Doorkeeper::OAuth
subject.redirect_uri = nil
expect(subject).not_to be_authorizable
end

describe "as_json" do
let(:client_id){ 'client_uid_123' }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing to the left of {.
Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

it 'sets redirect_uri to correct value' do
redirect_uri = JSON.parse(response.body)['redirect_uri']
expect(redirect_uri).to_not be_nil
expect(redirect_uri.match(/token_type=(\w+)&?/)[1]).to eq 'bearer'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

end

it 'sets redirect_uri to correct value' do
redirect_uri = JSON.parse(response.body)['redirect_uri']

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

expect(JSON.parse(response.body)['status']).to eq('redirect')
end

it 'sets redirect_uri to correct value' do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

end

it 'sets status to redirect' do
expect(JSON.parse(response.body)['status']).to eq('redirect')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

expect(Doorkeeper::AccessToken.count).to be 1
end

it 'sets status to redirect' do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

@@ -151,5 +151,25 @@ module Doorkeeper::OAuth
subject.redirect_uri = nil
expect(subject).not_to be_authorizable
end

describe 'as_json' do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

@@ -331,4 +331,18 @@
it { expect(Doorkeeper.configuration.base_controller).to eq('ApplicationController') }
end
end

describe 'api_mode' do
it 'is false by default' do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

@@ -331,4 +331,18 @@
it { expect(Doorkeeper.configuration.base_controller).to eq('ApplicationController') }
end
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace detected.

expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id)
end

it 'issues the token for the current resource owner' do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

@JoeWoodward
Copy link

No movement on this PR?

@nbulaj nbulaj mentioned this pull request Jan 27, 2018
@nbulaj nbulaj self-assigned this Jan 27, 2018
@nbulaj
Copy link
Member

nbulaj commented Jan 27, 2018

Hi @Borzik . I like this approach more than original PR. Does it still WIP , maybe you need some help? I will look at your questions (aka side-effects) ASAP.

@Borzik
Copy link
Contributor Author

Borzik commented Jan 27, 2018

Hi! When I submitted this PR I was working on a feature which was postponed, so I'm not working actively on this at the moment. Since you think this PR is worth attention, I'll look into it again next week, will rebase it on current master, and will try to get it out of WIP status (as I remember, it worked well for me on my app, but it still needs refactoring).

@nbulaj
Copy link
Member

nbulaj commented Jan 27, 2018

Great! Let me know it you will need some help.

@JoeWoodward
Copy link

JoeWoodward commented Feb 2, 2018

Just found another issue with this configuration. I have an app that is api only but also one that is api and web. The api only works fine, however, the hybrid I want to remove protect_from forgery for authorization & token & revoke endpoints but would need to still have the routes defined for applications

I think removing the routes should be separately configurable

@Borzik Borzik force-pushed the rails-api-support branch from c8e3f15 to df79d14 Compare February 2, 2018 12:23
@@ -200,6 +272,44 @@ def translated_error_message(key)
end
end

describe 'GET #new in API mode with skip_authorization true' do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Block has too many lines. [29/25]

end

it 'includes correct redirect URI' do
expect(redirect_uri).to match(%r{^#{client.redirect_uri}})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use // around regular expression.

post :create, client_id: client.uid, response_type: 'token', scope: 'invalid', redirect_uri: client.redirect_uri
end
let(:response_json_body){ JSON.parse(response.body) }
let(:redirect_uri){ response_json_body['redirect_uri'] }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing to the left of {.

default_scopes_exist :public
post :create, client_id: client.uid, response_type: 'token', scope: 'invalid', redirect_uri: client.redirect_uri
end
let(:response_json_body){ JSON.parse(response.body) }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing to the left of {.

end

it 'renders correct redirect uri' do
expect(redirect_uri).to match(%r{^#{client.redirect_uri}})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use // around regular expression.

@@ -5,7 +5,8 @@ class ErrorResponse < BaseResponse

def self.from_request(request, attributes = {})
state = request.state if request.respond_to?(:state)
new(attributes.merge(name: request.error, state: state))
redirect_uri = request.redirect_uri if request.respond_to?(:redirect_uri)
new(attributes.merge(name: request.error, state: state, redirect_uri: redirect_uri))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [92/80]

@@ -5,7 +5,8 @@ class ErrorResponse < BaseResponse

def self.from_request(request, attributes = {})
state = request.state if request.respond_to?(:state)
new(attributes.merge(name: request.error, state: state))
redirect_uri = request.redirect_uri if request.respond_to?(:redirect_uri)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [81/80]

redirect_to auth.redirect_uri
else
render json: auth.body, status: auth.status
return render(json: { status: :redirect, redirect_uri: auth.redirect_uri }, status: auth.status) if Doorkeeper.configuration.api_mode

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [141/80]

end
else
render :error
if Doorkeeper.configuration.api_mode
render json: @pre_auth.error_response.body[:error_description], status: :bad_request

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [94/80]

end
else
render :error
if Doorkeeper.configuration.api_mode

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convert if nested inside else to elsif.

@@ -200,6 +272,44 @@ def translated_error_message(key)
end
end

describe 'GET #new in API mode with skip_authorization true' do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Block has too many lines. [29/25]

end

it 'includes correct redirect URI' do
expect(redirect_uri).to match(%r{^#{client.redirect_uri}})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use // around regular expression.

post :create, client_id: client.uid, response_type: 'token', scope: 'invalid', redirect_uri: client.redirect_uri
end
let(:response_json_body){ JSON.parse(response.body) }
let(:redirect_uri){ response_json_body['redirect_uri'] }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing to the left of {.

default_scopes_exist :public
post :create, client_id: client.uid, response_type: 'token', scope: 'invalid', redirect_uri: client.redirect_uri
end
let(:response_json_body){ JSON.parse(response.body) }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing to the left of {.

end

it 'renders correct redirect uri' do
expect(redirect_uri).to match(%r{^#{client.redirect_uri}})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use // around regular expression.

@@ -5,7 +5,8 @@ class ErrorResponse < BaseResponse

def self.from_request(request, attributes = {})
state = request.state if request.respond_to?(:state)
new(attributes.merge(name: request.error, state: state))
redirect_uri = request.redirect_uri if request.respond_to?(:redirect_uri)
new(attributes.merge(name: request.error, state: state, redirect_uri: redirect_uri))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [92/80]

@@ -5,7 +5,8 @@ class ErrorResponse < BaseResponse

def self.from_request(request, attributes = {})
state = request.state if request.respond_to?(:state)
new(attributes.merge(name: request.error, state: state))
redirect_uri = request.redirect_uri if request.respond_to?(:redirect_uri)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [81/80]

redirect_to auth.redirect_uri
else
render json: auth.body, status: auth.status
return render(json: { status: :redirect, redirect_uri: auth.redirect_uri }, status: auth.status) if Doorkeeper.configuration.api_mode

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [141/80]

end
else
render :error
if Doorkeeper.configuration.api_mode
render json: @pre_auth.error_response.body[:error_description], status: :bad_request

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [94/80]

end
else
render :error
if Doorkeeper.configuration.api_mode

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convert if nested inside else to elsif.

@Borzik Borzik force-pushed the rails-api-support branch from df79d14 to 03cdd92 Compare February 2, 2018 12:43
@@ -200,6 +272,46 @@ def translated_error_message(key)
end
end

describe 'GET #new in API mode with skip_authorization true' do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Block has too many lines. [31/25]

name: request.error,
state: state,
redirect_uri: redirect_uri
}))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indent the right brace the same as the first position after the preceding left parenthesis.

redirect_uri = request.redirect_uri
end
new(attributes.merge({
name: request.error,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use 2 spaces for indentation in a hash, relative to the first position after the preceding left parenthesis.

if request.respond_to?(:redirect_uri)
redirect_uri = request.redirect_uri
end
new(attributes.merge({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant curly braces around a hash parameter.

redirect_to auth.redirect_uri
else
render json: auth.body, status: auth.status
return render(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Favor a normal if-statement over a modifier clause in a multiline statement.

else
render :new
end
else
render :error
if Doorkeeper.configuration.api_mode

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convert if nested inside else to elsif.

@Borzik Borzik force-pushed the rails-api-support branch 3 times, most recently from a49d8aa to d5849e7 Compare February 2, 2018 13:00
redirect_uri = request.redirect_uri
end
new(attributes.merge(
name: request.error,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indent the first parameter one step more than attributes.merge(.

@Borzik Borzik force-pushed the rails-api-support branch 4 times, most recently from ca7debb to 2a3036e Compare February 2, 2018 13:08
@Borzik
Copy link
Contributor Author

Borzik commented Feb 2, 2018

@JoeWoodward
I think such hybrid app support is out of this PR's scope. This update is intended to support Rails applications generated with --api argument only, and to do it in the simplest possible way (by toggling one configuration line).

@nbulaj
I've done some cleanup and fixed most Hound comments.
I don't really like what's going on in app/controllers/doorkeeper/authorizations_controller.rb, specifically this structure:

if success
  if api_mode?
  else
  end
else
  if api_mode?
  else
  end
end

And redirectable? doesn't seem to convey correct meaning here:

if redirectable? and api_mode?
  render ...
elsif redirectable?
  redirect
else
  render
end

If you have any ideas how this can be improved, please let me know.

@nbulaj nbulaj added the feature label Feb 4, 2018
@nbulaj nbulaj added this to the 5.0 milestone Feb 13, 2018
@nbulaj
Copy link
Member

nbulaj commented Feb 16, 2018

Hi @Borzik. Can you please resolve the conflicts?

What about some code issues, you can extract successful and error handling to separate private methods, like

def new
  if pre_auth.authorizable?
    render_successful
   else
     render_error
  end
end

private 

# ...

# Nothing wrong with if elsif else :)
def render_successful
  if skip_authorization? || matching_token?
    auth = authorization.authorize
    redirect_or_render auth
  elsif Doorkeeper.configuration.api_mode
    render json: @pre_auth
  else
    render :new
  end
end

def render_error
  if Doorkeeper.configuration.api_mode
    render json: @pre_auth.error_response.body[:error_description],
           status: :bad_request
  else
    render :error
  end
end

@nbulaj
Copy link
Member

nbulaj commented Feb 27, 2018

Hi @Borzik . Could you please rebase with the master to resolve the conflicts? I think it is ready to be merged

@pacop
Copy link

pacop commented Mar 5, 2018

Interested in seeing this in the new version. Hope that @Borzik can rebase these files. Is sth that could be done to help?

@Borzik Borzik force-pushed the rails-api-support branch from 2a3036e to ca36a93 Compare March 5, 2018 11:55
@@ -4,7 +4,11 @@ class ErrorResponse < BaseResponse
include OAuth::Helpers

def self.from_request(request, attributes = {})
new(attributes.merge(name: request.error, state: request.try(:state)))
new(attributes.merge(
name: request.error,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indent the first parameter one step more than attributes.merge(.

@Borzik Borzik force-pushed the rails-api-support branch from ca36a93 to 26d4543 Compare March 5, 2018 12:04
def render_error
if Doorkeeper.configuration.api_mode
render json: @pre_auth.error_response.body[:error_description],
status: :bad_request

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Align the elements of a hash literal if they span more than one line.

@Borzik Borzik force-pushed the rails-api-support branch from 26d4543 to 00fc954 Compare March 5, 2018 12:05
@Borzik
Copy link
Contributor Author

Borzik commented Mar 5, 2018

@nbulaj @pacop Done

@Borzik Borzik changed the title [WIP] Rails API support Rails API support Mar 5, 2018
@nbulaj nbulaj merged commit ba374aa into doorkeeper-gem:master Mar 5, 2018
@nbulaj
Copy link
Member

nbulaj commented Mar 5, 2018

Thanks @Borzik ! It would be really great if somebody could test it in his app, maybe @pacop ? Try to use master branch to be sure we didn't break something

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants