diff --git a/Gemfile b/Gemfile
index 901595f59..e9bfa83c7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -28,6 +28,7 @@ end
gem 'autoprefixer-rails'
+gem 'doorkeeper'
gem 'oauth2'
gem 'omniauth'
diff --git a/Gemfile.lock b/Gemfile.lock
index 7c3f059c6..06d4ccbb8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -82,6 +82,8 @@ GEM
docile (1.1.5)
domain_name (0.5.20160826)
unf (>= 0.0.5, < 1.0.0)
+ doorkeeper (4.2.0)
+ railties (>= 4.2)
eco (1.0.0)
coffee-script
eco-source
@@ -350,6 +352,7 @@ DEPENDENCIES
daemons
delayed_job_active_record
diffy
+ doorkeeper
eco
em-websocket
email_verifier
diff --git a/app/assets/javascripts/app/models/application.coffee b/app/assets/javascripts/app/models/application.coffee
index 85fd37030..4bad86d8b 100644
--- a/app/assets/javascripts/app/models/application.coffee
+++ b/app/assets/javascripts/app/models/application.coffee
@@ -1,17 +1,17 @@
class App.Application extends App.Model
- @configure 'Application', 'name', 'scopes', 'redirect_uri'
+ @configure 'Application', 'name', 'redirect_uri', 'uid', 'secret'
@extend Spine.Model.Ajax
@url: @apiPath + '/applications'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'redirect_uri', display: 'Redirect URI', tag: 'textarea', limit: 250, null: false, note: 'Use one line per URI' },
- { name: 'scopes', display: 'Scopes', tag: 'input', note: 'Scopes define the access for' },
- { name: 'clients', display: 'Clients', tag: 'input', readonly: 1 },
+ { name: 'uid', display: 'Application ID', tag: 'input', type: 'text', null: true, readonly: 1 },
+ { name: 'secret', display: 'Application secret', tag: 'input', type: 'text', null: true },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
@configure_overview = [
- 'name', 'scopes', 'clients'
+ 'name', 'uid'
]
@configure_delete = true
diff --git a/app/assets/javascripts/app/views/api.jst.eco b/app/assets/javascripts/app/views/api.jst.eco
index ddffb1fd9..86f412824 100644
--- a/app/assets/javascripts/app/views/api.jst.eco
+++ b/app/assets/javascripts/app/views/api.jst.eco
@@ -59,7 +59,7 @@ OAuth URLs are:
- <%- @T('Title') %>
+ | <%- @T('Action') %>
| <%- @T('URL') %>
|
@@ -69,9 +69,6 @@ OAuth URLs are:
<%- @T('Getting an Access Token') %>
| <%= @C('http_type') %>://<%= @C('fqdn') %>/oauth/token
- |
- <%- @T('Revoking Access') %>
- | <%= @C('http_type') %>://<%= @C('fqdn') %>/oauth/applications
|
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b732d50aa..3c84b465c 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -301,7 +301,6 @@ class ApplicationController < ActionController::Base
return authentication_check_prerequesits(user, 'token_auth', auth_param) if user
end
-=begin
# check oauth2 token based authentication
token = Doorkeeper::OAuth::Token.from_bearer_authorization(request)
if token
@@ -314,14 +313,14 @@ class ApplicationController < ActionController::Base
raise Exceptions::NotAuthorized, 'OAuth2 token is expired!'
end
- if access_token.scopes.empty?
- raise Exceptions::NotAuthorized, 'OAuth2 scope missing for token!'
- end
+ # if access_token.scopes.empty?
+ # raise Exceptions::NotAuthorized, 'OAuth2 scope missing for token!'
+ # end
user = User.find(access_token.resource_owner_id)
return authentication_check_prerequesits(user, 'token_auth', auth_param) if user
end
-=end
+
false
end
diff --git a/app/controllers/applications_controller.rb b/app/controllers/applications_controller.rb
new file mode 100644
index 000000000..00da58b5e
--- /dev/null
+++ b/app/controllers/applications_controller.rb
@@ -0,0 +1,64 @@
+# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
+
+class ApplicationsController < ApplicationController
+ before_action { authentication_check(permission: 'admin.api') }
+
+ def index
+ all = Doorkeeper::Application.all
+ if params[:full]
+ assets = {}
+ item_ids = []
+ all.each { |item|
+ item_ids.push item.id
+ if !assets[:Application]
+ assets[:Application] = {}
+ end
+ assets[:Application][item.id] = item.attributes
+ }
+ render json: {
+ record_ids: item_ids,
+ assets: assets,
+ }, status: :ok
+ return
+ end
+
+ render json: all, status: :ok
+ end
+
+ def show
+ application = Doorkeeper::Application.find(params[:id])
+ render json: application, status: :ok
+ end
+
+ def create
+ application = Doorkeeper::Application.new(clean_params)
+ application.save!
+ render json: application, status: :ok
+ end
+
+ def update
+ application = Doorkeeper::Application.find(params[:id])
+ application.update_attributes!(clean_params)
+ render json: application, status: :ok
+ end
+
+ def destroy
+ application = Doorkeeper::Application.find(params[:id])
+ application.destroy!
+ render json: {}, status: :ok
+ end
+
+ private
+
+ def clean_params
+ params_data = params.permit! #.to_h
+ params_data.delete('application')
+ params_data.delete('action')
+ params_data.delete('controller')
+ params_data.delete('id')
+ params_data.delete('uid')
+ params_data.delete('secret')
+ params_data.delete('created_at')
+ params_data
+ end
+end
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
new file mode 100644
index 000000000..4eeb378e4
--- /dev/null
+++ b/config/initializers/doorkeeper.rb
@@ -0,0 +1,112 @@
+Doorkeeper.configure do
+ # Change the ORM that doorkeeper will use (needs plugins)
+ orm :active_record
+
+ # This block will be called to check whether the resource owner is authenticated or not.
+ resource_owner_authenticator do
+ # fail "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}"
+ # Put your resource owner authentication logic here.
+ # Example implementation:
+ User.find_by_id(session[:user_id]) || redirect_to(new_user_session_url)
+ end
+
+ # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
+ # admin_authenticator do
+ # # Put your admin authentication logic here.
+ # # Example implementation:
+ # Admin.find_by_id(session[:admin_id]) || redirect_to(new_admin_session_url)
+ # end
+
+ # Authorization Code expiration time (default 10 minutes).
+ # authorization_code_expires_in 10.minutes
+
+ # Access token expiration time (default 2 hours).
+ # If you want to disable expiration, set this to nil.
+ # access_token_expires_in 2.hours
+
+ # Assign a custom TTL for implicit grants.
+ # custom_access_token_expires_in do |oauth_client|
+ # oauth_client.application.additional_settings.implicit_oauth_expiration
+ # end
+
+ # Use a custom class for generating the access token.
+ # https://github.com/doorkeeper-gem/doorkeeper#custom-access-token-generator
+ # access_token_generator '::Doorkeeper::JWT'
+
+ # The controller Doorkeeper::ApplicationController inherits from.
+ # Defaults to ActionController::Base.
+ # https://github.com/doorkeeper-gem/doorkeeper#custom-base-controller
+ # base_controller 'ApplicationController'
+
+ # Reuse access token for the same resource owner within an application (disabled by default)
+ # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383
+ # reuse_access_token
+
+ # Issue access tokens with refresh token (disabled by default)
+ # use_refresh_token
+
+ # Provide support for an owner to be assigned to each registered application (disabled by default)
+ # Optional parameter confirmation: true (default false) if you want to enforce ownership of
+ # a registered application
+ # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support
+ # enable_application_owner confirmation: false
+
+ # Define access token scopes for your provider
+ # For more information go to
+ # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
+ # default_scopes :public
+ # optional_scopes :write, :update
+
+ # Change the way client credentials are retrieved from the request object.
+ # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
+ # falls back to the `:client_id` and `:client_secret` params from the `params` object.
+ # Check out the wiki for more information on customization
+ # client_credentials :from_basic, :from_params
+
+ # Change the way access token is authenticated from the request object.
+ # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
+ # falls back to the `:access_token` or `:bearer_token` params from the `params` object.
+ # Check out the wiki for more information on customization
+ # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param
+
+ # Change the native redirect uri for client apps
+ # When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider
+ # The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL
+ # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi)
+ #
+ # native_redirect_uri 'urn:ietf:wg:oauth:2.0:oob'
+
+ # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled
+ # by default in non-development environments). OAuth2 delegates security in
+ # communication to the HTTPS protocol so it is wise to keep this enabled.
+ #
+ # force_ssl_in_redirect_uri !Rails.env.development?
+
+ # Specify what grant flows are enabled in array of Strings. The valid
+ # strings and the flows they enable are:
+ #
+ # "authorization_code" => Authorization Code Grant Flow
+ # "implicit" => Implicit Grant Flow
+ # "password" => Resource Owner Password Credentials Grant Flow
+ # "client_credentials" => Client Credentials Grant Flow
+ #
+ # If not specified, Doorkeeper enables authorization_code and
+ # client_credentials.
+ #
+ # implicit and password grant flows have risks that you should understand
+ # before enabling:
+ # http://tools.ietf.org/html/rfc6819#section-4.4.2
+ # http://tools.ietf.org/html/rfc6819#section-4.4.3
+ #
+ # grant_flows %w(authorization_code client_credentials)
+
+ # Under some circumstances you might want to have applications auto-approved,
+ # so that the user skips the authorization step.
+ # For example if dealing with a trusted application.
+ # skip_authorization do |resource_owner, client|
+ # client.superapp? or resource_owner.admin?
+ # end
+
+ # WWW-Authenticate Realm (default "Doorkeeper").
+ # realm "Doorkeeper"
+end
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
new file mode 100644
index 000000000..e67b778be
--- /dev/null
+++ b/config/locales/doorkeeper.en.yml
@@ -0,0 +1,124 @@
+en:
+ activerecord:
+ attributes:
+ doorkeeper/application:
+ name: 'Name'
+ redirect_uri: 'Redirect URI'
+ errors:
+ models:
+ doorkeeper/application:
+ attributes:
+ redirect_uri:
+ fragment_present: 'cannot contain a fragment.'
+ invalid_uri: 'must be a valid URI.'
+ relative_uri: 'must be an absolute URI.'
+ secured_uri: 'must be an HTTPS/SSL URI.'
+
+ doorkeeper:
+ applications:
+ confirmations:
+ destroy: 'Are you sure?'
+ buttons:
+ edit: 'Edit'
+ destroy: 'Destroy'
+ submit: 'Submit'
+ cancel: 'Cancel'
+ authorize: 'Authorize'
+ form:
+ error: 'Whoops! Check your form for possible errors'
+ help:
+ redirect_uri: 'Use one line per URI'
+ native_redirect_uri: 'Use %{native_redirect_uri} for local tests'
+ scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.'
+ edit:
+ title: 'Edit application'
+ index:
+ title: 'Your applications'
+ new: 'New Application'
+ name: 'Name'
+ callback_url: 'Callback URL'
+ new:
+ title: 'New Application'
+ show:
+ title: 'Application: %{name}'
+ application_id: 'Application Id'
+ secret: 'Secret'
+ scopes: 'Scopes'
+ callback_urls: 'Callback urls'
+ actions: 'Actions'
+
+ authorizations:
+ buttons:
+ authorize: 'Authorize'
+ deny: 'Deny'
+ error:
+ title: 'An error has occurred'
+ new:
+ title: 'Authorization required'
+ prompt: 'Authorize %{client_name} to use your account?'
+ able_to: 'This application will be able to'
+ show:
+ title: 'Authorization code'
+
+ authorized_applications:
+ confirmations:
+ revoke: 'Are you sure?'
+ buttons:
+ revoke: 'Revoke'
+ index:
+ title: 'Your authorized applications'
+ application: 'Application'
+ created_at: 'Created At'
+ date_format: '%Y-%m-%d %H:%M:%S'
+
+ errors:
+ messages:
+ # Common error messages
+ invalid_request: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.'
+ invalid_redirect_uri: 'The redirect uri included is not valid.'
+ unauthorized_client: 'The client is not authorized to perform this request using this method.'
+ access_denied: 'The resource owner or authorization server denied the request.'
+ invalid_scope: 'The requested scope is invalid, unknown, or malformed.'
+ server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.'
+ temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.'
+
+ #configuration error messages
+ credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.'
+ resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.'
+
+ # Access grant errors
+ unsupported_response_type: 'The authorization server does not support this response type.'
+
+ # Access token errors
+ invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.'
+ invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.'
+ unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.'
+
+ # Password Access token errors
+ invalid_resource_owner: 'The provided resource owner credentials are not valid, or resource owner cannot be found'
+
+ invalid_token:
+ revoked: "The access token was revoked"
+ expired: "The access token expired"
+ unknown: "The access token is invalid"
+
+ flash:
+ applications:
+ create:
+ notice: 'Application created.'
+ destroy:
+ notice: 'Application deleted.'
+ update:
+ notice: 'Application updated.'
+ authorized_applications:
+ destroy:
+ notice: 'Application revoked.'
+
+ layouts:
+ admin:
+ nav:
+ oauth2_provider: 'OAuth2 Provider'
+ applications: 'Applications'
+ home: 'Home'
+ application:
+ title: 'OAuth authorization required'
diff --git a/config/routes/doorkeeper.rb b/config/routes/doorkeeper.rb
new file mode 100644
index 000000000..9acd9e820
--- /dev/null
+++ b/config/routes/doorkeeper.rb
@@ -0,0 +1,13 @@
+Zammad::Application.routes.draw do
+ api_path = Rails.configuration.api_path
+
+ match api_path + '/applications', to: 'applications#index', via: :get
+ match api_path + '/applications/:id', to: 'applications#show', via: :get
+ match api_path + '/applications', to: 'applications#create', via: :post
+ match api_path + '/applications/:id', to: 'applications#update', via: :put
+
+ # oauth2 provider routes
+ use_doorkeeper do
+ skip_controllers :applications, :authorized_applications
+ end
+end
diff --git a/db/migrate/20161101131409_create_doorkeeper_tables.rb b/db/migrate/20161101131409_create_doorkeeper_tables.rb
new file mode 100644
index 000000000..da7c84353
--- /dev/null
+++ b/db/migrate/20161101131409_create_doorkeeper_tables.rb
@@ -0,0 +1,68 @@
+class CreateDoorkeeperTables < ActiveRecord::Migration
+ def change
+ create_table :oauth_applications do |t|
+ t.string :name, null: false
+ t.string :uid, null: false
+ t.string :secret, null: false
+ t.text :redirect_uri, null: false
+ t.string :scopes, null: false, default: ''
+ t.timestamps null: false
+ end
+
+ add_index :oauth_applications, :uid, unique: true
+
+ create_table :oauth_access_grants do |t|
+ t.integer :resource_owner_id, null: false
+ t.references :application, null: false
+ t.string :token, null: false
+ t.integer :expires_in, null: false
+ t.text :redirect_uri, null: false
+ t.datetime :created_at, null: false
+ t.datetime :revoked_at
+ t.string :scopes
+ end
+
+ add_index :oauth_access_grants, :token, unique: true
+ add_foreign_key(
+ :oauth_access_grants,
+ :oauth_applications,
+ column: :application_id
+ )
+
+ create_table :oauth_access_tokens do |t|
+ t.integer :resource_owner_id
+ t.references :application
+
+ # If you use a custom token generator you may need to change this column
+ # from string to text, so that it accepts tokens larger than 255
+ # characters. More info on custom token generators in:
+ # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator
+ #
+ # t.text :token, null: false
+ t.string :token, null: false
+
+ t.string :refresh_token
+ t.integer :expires_in
+ t.datetime :revoked_at
+ t.datetime :created_at, null: false
+ t.string :scopes
+
+ # If there is a previous_refresh_token column,
+ # refresh tokens will be revoked after a related access token is used.
+ # If there is no previous_refresh_token column,
+ # previous tokens are revoked as soon as a new access token is created.
+ # Comment out this line if you'd rather have refresh tokens
+ # instantly revoked.
+ t.string :previous_refresh_token, null: false, default: ''
+ end
+
+ add_index :oauth_access_tokens, :token, unique: true
+ add_index :oauth_access_tokens, :resource_owner_id
+ add_index :oauth_access_tokens, :refresh_token, unique: true
+ add_foreign_key(
+ :oauth_access_tokens,
+ :oauth_applications,
+ column: :application_id
+ )
+ end
+end