Skip to content

Commit

Permalink
Allow public clients to authenticate without client_secret
Browse files Browse the repository at this point in the history
Add Application#confidential

Add dummy migration for Application#confidential

Because Dummy app is now Rails 5.1, the old migrations' ancestor class needed to be explicitly the 4.2 variety.

Expose app confidentiality in views & controllers

Allow public applications to be found if secret is blank

Don't use #client_via_uid fallback on Password strategy

Since credentials will only contain UID when a public application is calling, the fallback method of finding by UID alone is dead code. Private apps are not allowed to be identified by UID alone.
  • Loading branch information
Justin Bull committed Jul 11, 2018
1 parent e29441b commit c5ecad2
Show file tree
Hide file tree
Showing 21 changed files with 190 additions and 48 deletions.
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ AllCops:
Exclude:
- "spec/dummy/db/*"

Metrics/BlockLength:
Exclude:
- spec/**/*

LineLength:
Exclude:
- spec/**/*
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/doorkeeper/applications_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ def set_application
end

def application_params
params.require(:doorkeeper_application).permit(:name, :redirect_uri, :scopes)
params.require(:doorkeeper_application).
permit(:name, :redirect_uri, :scopes, :confidential)
end
end
end
11 changes: 11 additions & 0 deletions app/views/doorkeeper/applications/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@
</div>
<% end %>

<%= content_tag :div, class: "form-group#{' has-error' if application.errors[:confidential].present?}" do %>
<%= f.label :confidential, class: 'col-sm-2 control-label' %>
<div class="col-sm-10">
<%= f.check_box :confidential, class: 'form-control' %>
<%= doorkeeper_errors_for application, :confidential %>
<span class="help-block">
<%= t('doorkeeper.applications.help.confidential') %>
</span>
</div>
<% end %>

<%= content_tag :div, class: "form-group#{' has-error' if application.errors[:scopes].present?}" do %>
<%= f.label :scopes, class: 'col-sm-2 control-label' %>
<div class="col-sm-10">
Expand Down
2 changes: 2 additions & 0 deletions app/views/doorkeeper/applications/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<tr>
<th><%= t('.name') %></th>
<th><%= t('.callback_url') %></th>
<th><%= t('.confidential') %></th>
<th></th>
<th></th>
</tr>
Expand All @@ -18,6 +19,7 @@
<tr id="application_<%= application.id %>">
<td><%= link_to application.name, oauth_application_path(application) %></td>
<td><%= application.redirect_uri %></td>
<td><%= application.confidential? ? t('doorkeeper.applications.index.confidentiality.yes') : t('doorkeeper.applications.index.confidentiality.no') %></td>
<td><%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(application), class: 'btn btn-link' %></td>
<td><%= render 'delete_form', application: application %></td>
</tr>
Expand Down
3 changes: 3 additions & 0 deletions app/views/doorkeeper/applications/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
<h4><%= t('.scopes') %>:</h4>
<p><code id="scopes"><%= @application.scopes %></code></p>

<h4><%= t('.confidential') %>:</h4>
<p><code id="confidential"><%= @application.confidential? %></code></p>

<h4><%= t('.callback_urls') %>:</h4>

<table>
Expand Down
6 changes: 6 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ en:
form:
error: 'Whoops! Check your form for possible errors'
help:
confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.'
redirect_uri: 'Use one line per URI'
native_redirect_uri: 'Use %{native_redirect_uri} if you want to add localhost URIs for development purposes'
scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.'
Expand All @@ -38,13 +39,18 @@ en:
new: 'New Application'
name: 'Name'
callback_url: 'Callback URL'
confidential: 'Confidential?'
confidentiality:
yes: 'Yes'
no: 'No'
new:
title: 'New Application'
show:
title: 'Application: %{name}'
application_id: 'Application Id'
secret: 'Secret'
scopes: 'Scopes'
confidential: 'Confidential'
callback_urls: 'Callback urls'
actions: 'Actions'

Expand Down
9 changes: 8 additions & 1 deletion lib/doorkeeper/models/application_mixin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,21 @@ module ClassMethods
# Returns an instance of the Doorkeeper::Application with
# specific UID and secret.
#
# Public/Non-confidential applications will only find by uid if secret is
# blank.
#
# @param uid [#to_s] UID (any object that responds to `#to_s`)
# @param secret [#to_s] secret (any object that responds to `#to_s`)
#
# @return [Doorkeeper::Application, nil] Application instance or nil
# if there is no record with such credentials
#
def by_uid_and_secret(uid, secret)
find_by(uid: uid.to_s, secret: secret.to_s)
app = by_uid(uid)
return unless app
return app if secret.blank? && !app.confidential?
return unless app.secret == secret
app
end

# Returns an instance of the Doorkeeper::Application with specific UID.
Expand Down
4 changes: 3 additions & 1 deletion lib/doorkeeper/oauth/client/credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ def from_basic(request)
end
end

# Public clients may have their secret blank, but "credentials" are
# still present
def blank?
uid.blank? || secret.blank?
uid.blank?
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/doorkeeper/orm/active_record/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Application < ActiveRecord::Base
validates :name, :secret, :uid, presence: true
validates :uid, uniqueness: true
validates :redirect_uri, redirect_uri: true
validates :confidential, inclusion: { in: [true, false] }

before_validation :generate_uid, :generate_secret, on: :create

Expand Down
12 changes: 1 addition & 11 deletions lib/doorkeeper/request/password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module Doorkeeper
module Request
class Password < Strategy
delegate :credentials, :resource_owner, :parameters, to: :server
delegate :credentials, :resource_owner, :parameters, :client, to: :server

def request
@request ||= OAuth::PasswordAccessTokenRequest.new(
Expand All @@ -13,16 +13,6 @@ def request
parameters
)
end

private

def client
if credentials
server.client
elsif parameters[:client_id]
server.client_via_uid
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/generators/doorkeeper/templates/migration.rb.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class CreateDoorkeeperTables < ActiveRecord::Migration<%= migration_version %>
t.string :secret, null: false
t.text :redirect_uri, null: false
t.string :scopes, null: false, default: ''
t.boolean :confidential, null: false, default: true
t.timestamps null: false
end

Expand Down
4 changes: 3 additions & 1 deletion spec/dummy/db/migrate/20111122132257_create_users.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class CreateUsers < ActiveRecord::Migration
# frozen_string_literal: true

class CreateUsers < ActiveRecord::Migration[4.2]
def change
create_table :users do |t|
t.string :name
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class AddPasswordToUsers < ActiveRecord::Migration
# frozen_string_literal: true

class AddPasswordToUsers < ActiveRecord::Migration[4.2]
def change
add_column :users, :password, :string
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class CreateDoorkeeperTables < ActiveRecord::Migration
# frozen_string_literal: true

class CreateDoorkeeperTables < ActiveRecord::Migration[4.2]
def change
create_table :oauth_applications do |t|
t.string :name, null: false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class AddOwnerToApplication < ActiveRecord::Migration
# frozen_string_literal: true

class AddOwnerToApplication < ActiveRecord::Migration[4.2]
def change
add_column :oauth_applications, :owner_id, :integer, null: true
add_column :oauth_applications, :owner_type, :string, null: true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class AddPreviousRefreshTokenToAccessTokens < ActiveRecord::Migration
# frozen_string_literal: true

class AddPreviousRefreshTokenToAccessTokens < ActiveRecord::Migration[4.2]
def change
add_column(
:oauth_access_tokens,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

class AddConfidentialToApplication < ActiveRecord::Migration[5.1]
def change
add_column(
:oauth_applications,
:confidential,
:boolean,
null: false,
default: true # maintaining backwards compatibility: require secrets
)
end
end
3 changes: 2 additions & 1 deletion spec/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20160320211015) do
ActiveRecord::Schema.define(version: 20180210183654) do

create_table "oauth_access_grants", force: :cascade do |t|
t.integer "resource_owner_id", null: false
Expand Down Expand Up @@ -52,6 +52,7 @@
t.datetime "updated_at"
t.integer "owner_id"
t.string "owner_type"
t.boolean "confidential", default: true, null: false
end

add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type"
Expand Down
6 changes: 4 additions & 2 deletions spec/lib/oauth/client/credentials_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ class Doorkeeper::OAuth::Client
let(:client_id) { 'some-uid' }
let(:client_secret) { 'some-secret' }

it 'is blank when any of the credentials is blank' do
it 'is blank when the uid in credentials is blank' do
expect(Credentials.new(nil, nil)).to be_blank
expect(Credentials.new(nil, 'something')).to be_blank
expect(Credentials.new('something', nil)).to be_blank
expect(Credentials.new('something', nil)).to be_present
expect(Credentials.new('something', 'something')).to be_present
end

describe :from_request do
Expand Down
55 changes: 50 additions & 5 deletions spec/models/doorkeeper/application_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ module Doorkeeper
expect(new_application).not_to be_valid
end

it 'is invalid without determining confidentiality' do
new_application.confidential = nil
expect(new_application).not_to be_valid
end

it 'generates uid on create' do
expect(new_application.uid).to be_nil
new_application.save
Expand Down Expand Up @@ -201,11 +206,51 @@ module Doorkeeper
end
end

describe :authenticate do
it 'finds the application via uid/secret' do
app = FactoryBot.create :application
authenticated = Application.by_uid_and_secret(app.uid, app.secret)
expect(authenticated).to eq(app)
describe :by_uid_and_secret do
context "when application is private/confidential" do
it "finds the application via uid/secret" do
app = FactoryBot.create :application
authenticated = Application.by_uid_and_secret(app.uid, app.secret)
expect(authenticated).to eq(app)
end
context "when secret is wrong" do
it "should not find the application" do
app = FactoryBot.create :application
authenticated = Application.by_uid_and_secret(app.uid, 'bad')
expect(authenticated).to eq(nil)
end
end
end

context "when application is public/non-confidential" do
context "when secret is blank" do
it "should find the application" do
app = FactoryBot.create :application, confidential: false
authenticated = Application.by_uid_and_secret(app.uid, nil)
expect(authenticated).to eq(app)
end
end
context "when secret is wrong" do
it "should not find the application" do
app = FactoryBot.create :application, confidential: false
authenticated = Application.by_uid_and_secret(app.uid, 'bad')
expect(authenticated).to eq(nil)
end
end
end
end

describe :confidential? do
subject { FactoryBot.create(:application, confidential: confidential).confidential? }

context 'when application is private/confidential' do
let(:confidential) { true }
it { expect(subject).to eq(true) }
end

context 'when application is public/non-confidential' do
let(:confidential) { false }
it { expect(subject).to eq(false) }
end
end
end
Expand Down
Loading

0 comments on commit c5ecad2

Please sign in to comment.