Browse Source

Working on OAuth2 support with doorkeeper gem.

Thorsten Eckel 8 years ago
parent
commit
faafe0cc60

+ 1 - 0
Gemfile

@@ -28,6 +28,7 @@ end
 
 gem 'autoprefixer-rails'
 
+gem 'doorkeeper'
 gem 'oauth2'
 
 gem 'omniauth'

+ 3 - 0
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

+ 4 - 4
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

+ 1 - 4
app/assets/javascripts/app/views/api.jst.eco

@@ -59,7 +59,7 @@ OAuth URLs are:
   <table class="settings-list" style="width: 100%;">
     <thead>
       <tr>
-        <th width="40%"><%- @T('Title') %>
+        <th width="40%"><%- @T('Action') %>
         <th width="60%"><%- @T('URL') %>
     </thead>
     <tbody>
@@ -69,9 +69,6 @@ OAuth URLs are:
       <tr>
         <td><%- @T('Getting an Access Token') %>
         <td><%= @C('http_type') %>://<%= @C('fqdn') %>/oauth/token
-      <tr>
-        <td><%- @T('Revoking Access') %>
-        <td><%= @C('http_type') %>://<%= @C('fqdn') %>/oauth/applications
     </tbody>
   </table>
 </div>

+ 4 - 5
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
 

+ 64 - 0
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

+ 112 - 0
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

+ 124 - 0
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'

+ 13 - 0
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

+ 68 - 0
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