From 213e5c178097ea64e0d3fae206cde90011f25fac Mon Sep 17 00:00:00 2001 From: Saurabh Bhatia Date: Thu, 13 Feb 2014 14:51:23 +0800 Subject: [PATCH] API Keys and Authorization Framework --- Gemfile | 2 +- Gemfile.lock | 11 +-- app/assets/javascripts/clients.js.coffee | 3 + app/assets/stylesheets/clients.css.scss | 3 + app/assets/stylesheets/home.js.coffee | 19 +++++ app/assets/stylesheets/scaffolds.css.scss | 69 +++++++++++++++++ app/controllers/api/v1/base_controller.rb | 36 +++++++++ app/controllers/api/v1/clients_controller.rb | 18 +++++ .../api/v1/extensions_controller.rb | 4 +- .../api/v1/templates_controller.rb | 5 +- app/controllers/application_controller.rb | 2 +- app/controllers/clients_controller.rb | 74 +++++++++++++++++++ app/helpers/clients_helper.rb | 2 + app/models/api_key.rb | 16 ++++ app/models/client.rb | 9 +++ app/models/user.rb | 8 +- app/views/clients/_form.html.erb | 29 ++++++++ app/views/clients/edit.html.erb | 6 ++ app/views/clients/index.html.erb | 31 ++++++++ app/views/clients/index.json.jbuilder | 4 + app/views/clients/new.html.erb | 5 ++ app/views/clients/show.html.erb | 19 +++++ app/views/clients/show.json.jbuilder | 1 + config/application.rb | 3 + config/initializers/doorkeeper.rb | 5 ++ config/routes.rb | 8 +- rest_client.rb | 2 + test/controllers/clients_controller_test.rb | 49 ++++++++++++ test/fixtures/api_keys.yml | 7 ++ test/fixtures/clients.yml | 11 +++ test/helpers/clients_helper_test.rb | 4 + test/models/api_key_test.rb | 7 ++ test/models/client_test.rb | 7 ++ 33 files changed, 461 insertions(+), 18 deletions(-) create mode 100644 app/assets/javascripts/clients.js.coffee create mode 100644 app/assets/stylesheets/clients.css.scss create mode 100644 app/assets/stylesheets/home.js.coffee create mode 100644 app/assets/stylesheets/scaffolds.css.scss create mode 100644 app/controllers/api/v1/base_controller.rb create mode 100644 app/controllers/api/v1/clients_controller.rb create mode 100644 app/controllers/clients_controller.rb create mode 100644 app/helpers/clients_helper.rb create mode 100644 app/models/api_key.rb create mode 100644 app/models/client.rb create mode 100644 app/views/clients/_form.html.erb create mode 100644 app/views/clients/edit.html.erb create mode 100644 app/views/clients/index.html.erb create mode 100644 app/views/clients/index.json.jbuilder create mode 100644 app/views/clients/new.html.erb create mode 100644 app/views/clients/show.html.erb create mode 100644 app/views/clients/show.json.jbuilder create mode 100644 rest_client.rb create mode 100644 test/controllers/clients_controller_test.rb create mode 100644 test/fixtures/api_keys.yml create mode 100644 test/fixtures/clients.yml create mode 100644 test/helpers/clients_helper_test.rb create mode 100644 test/models/api_key_test.rb create mode 100644 test/models/client_test.rb diff --git a/Gemfile b/Gemfile index ba958fb..368e892 100644 --- a/Gemfile +++ b/Gemfile @@ -10,7 +10,7 @@ gem 'httparty' gem 'devise' gem 'warden' -gem 'doorkeeper', github: 'shinzui/doorkeeper' +gem 'rack-cors', :require => 'rack/cors' gem 'cancan' gem 'rolify', :github => 'EppO/rolify' diff --git a/Gemfile.lock b/Gemfile.lock index b3db77a..e9dd1c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,13 +46,6 @@ GIT origin (~> 2.0) tzinfo (~> 0.3.37) -GIT - remote: git://github.com/shinzui/doorkeeper.git - revision: 8f67bd06945983cf9fd1b883a09cb51bc9ffe32e - specs: - doorkeeper (1.0.0.rc1) - railties (>= 3.1) - GEM remote: https://rubygems.org/ specs: @@ -154,6 +147,8 @@ GEM puma (2.7.1) rack (>= 1.1, < 2.0) rack (1.5.2) + rack-cors (0.2.7) + rack rack-test (0.6.2) rack (>= 1.0) rails (4.0.2) @@ -218,7 +213,6 @@ DEPENDENCIES carrierwave-mongoid coffee-rails (~> 4.0.0) devise - doorkeeper! font-awesome-rails! httparty jbuilder (~> 1.2) @@ -228,6 +222,7 @@ DEPENDENCIES mongoid-grid_fs! mongoid_slug! puma + rack-cors rails (= 4.0.2) rmagick rolify! diff --git a/app/assets/javascripts/clients.js.coffee b/app/assets/javascripts/clients.js.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/clients.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/clients.css.scss b/app/assets/stylesheets/clients.css.scss new file mode 100644 index 0000000..6531617 --- /dev/null +++ b/app/assets/stylesheets/clients.css.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the clients controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/home.js.coffee b/app/assets/stylesheets/home.js.coffee new file mode 100644 index 0000000..5a1e141 --- /dev/null +++ b/app/assets/stylesheets/home.js.coffee @@ -0,0 +1,19 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ + +$.ajax({ + type: 'POST', + url: 'oauth/token.json', + # url: 'http://api.geonames.org/citiesJSON', + dataType: 'JSON', + data: { + grant_type: 'client_credentials', + client_id: '8dae7e34b1ba624e601cf659b65a70fa92d1c408d1f18252f9c0119b3efdce8d', + client_secret: 'e11386baaa4cd9a2327ce3a170ec7ae74d88c5ed618342852492f7603e065cb9' + }, + success: (d, textStatus, jqXHR) -> + console.debug d, textStatus + error: (jqXHR, textStatus, errorThrown) -> + console.debug jqXHR, textStatus, errorThrown +}) \ No newline at end of file diff --git a/app/assets/stylesheets/scaffolds.css.scss b/app/assets/stylesheets/scaffolds.css.scss new file mode 100644 index 0000000..6ec6a8f --- /dev/null +++ b/app/assets/stylesheets/scaffolds.css.scss @@ -0,0 +1,69 @@ +body { + background-color: #fff; + color: #333; + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +a { + color: #000; + &:visited { + color: #666; + } + &:hover { + color: #fff; + background-color: #000; + } +} + +div { + &.field, &.actions { + margin-bottom: 10px; + } +} + +#notice { + color: green; +} + +.field_with_errors { + padding: 2px; + background-color: red; + display: table; +} + +#error_explanation { + width: 450px; + border: 2px solid red; + padding: 7px; + padding-bottom: 0; + margin-bottom: 20px; + background-color: #f0f0f0; + h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px; + margin-bottom: 0px; + background-color: #c00; + color: #fff; + } + ul li { + font-size: 12px; + list-style: square; + } +} diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb new file mode 100644 index 0000000..b574c4d --- /dev/null +++ b/app/controllers/api/v1/base_controller.rb @@ -0,0 +1,36 @@ +module Api + module V1 + class BaseController < ApplicationController + before_filter :restrict_access + respond_to :json + skip_before_filter :verify_authenticity_token + + + def current_resource_owner + User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token + end + + private + def authorize_client + verify_client || render_unauthorized + end + + def restrict_access + authenticate_or_request_with_http_token do |token, options| + ApiKey.pluck(:access_token).include?(token) + end + end + + def verify_client + site_token = request.headers[:HTTP_X_SITETOKEN] + site_id = request.headers[:HTTP_X_SITEID] + client_status = Client.where(site_token: site_token).where(site_id: site_id).present? + end + + def render_unauthorized + self.headers['WWW-Authenticate'] = 'Token realm="Application"' + render json: 'Bad credentials', status: 401 + end + end + end +end diff --git a/app/controllers/api/v1/clients_controller.rb b/app/controllers/api/v1/clients_controller.rb new file mode 100644 index 0000000..9e003d8 --- /dev/null +++ b/app/controllers/api/v1/clients_controller.rb @@ -0,0 +1,18 @@ +module Api + module V1 + class ClientsController < Api::V1::BaseController + respond_to :json + + def create + respond_with Client.create(client_params) + end + + private + + # Never trust parameters from the scary internet, only allow the white list through. + def client_params + params.require(:client).permit(:site_name, :site_token, :site_id) + end + end + end +end diff --git a/app/controllers/api/v1/extensions_controller.rb b/app/controllers/api/v1/extensions_controller.rb index 58dde2a..16c4b45 100644 --- a/app/controllers/api/v1/extensions_controller.rb +++ b/app/controllers/api/v1/extensions_controller.rb @@ -1,7 +1,7 @@ module Api module V1 - class ExtensionsController < ApplicationController - # doorkeeper_for :all + class ExtensionsController < Api::V1::BaseController + before_action :authorize_client respond_to :json def index diff --git a/app/controllers/api/v1/templates_controller.rb b/app/controllers/api/v1/templates_controller.rb index 7ca7667..348b23e 100644 --- a/app/controllers/api/v1/templates_controller.rb +++ b/app/controllers/api/v1/templates_controller.rb @@ -1,7 +1,8 @@ module Api module V1 - class TemplatesController < ApplicationController - # doorkeeper_for :all, :scopes => [:public] + class TemplatesController < Api::V1::BaseController + before_action :authorize_client + respond_to :json def index diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d83690e..9e7c494 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,5 @@ class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. - protect_from_forgery with: :exception + protect_from_forgery with: :null_session end diff --git a/app/controllers/clients_controller.rb b/app/controllers/clients_controller.rb new file mode 100644 index 0000000..a274bed --- /dev/null +++ b/app/controllers/clients_controller.rb @@ -0,0 +1,74 @@ +class ClientsController < ApplicationController + before_action :set_client, only: [:show, :edit, :update, :destroy] + + # GET /clients + # GET /clients.json + def index + @clients = Client.all + end + + # GET /clients/1 + # GET /clients/1.json + def show + end + + # GET /clients/new + def new + @client = Client.new + end + + # GET /clients/1/edit + def edit + end + + # POST /clients + # POST /clients.json + def create + @client = Client.new(client_params) + + respond_to do |format| + if @client.save + format.html { redirect_to @client, notice: 'Client was successfully created.' } + format.json { render action: 'show', status: :created, location: @client } + else + format.html { render action: 'new' } + format.json { render json: @client.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /clients/1 + # PATCH/PUT /clients/1.json + def update + respond_to do |format| + if @client.update(client_params) + format.html { redirect_to @client, notice: 'Client was successfully updated.' } + format.json { head :no_content } + else + format.html { render action: 'edit' } + format.json { render json: @client.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /clients/1 + # DELETE /clients/1.json + def destroy + @client.destroy + respond_to do |format| + format.html { redirect_to clients_url } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_client + @client = Client.find(params[:id]) + end + + # Never trust parameters from the scary internet, only allow the white list through. + def client_params + params.require(:client).permit(:site_name, :site_token, :site_id) + end +end diff --git a/app/helpers/clients_helper.rb b/app/helpers/clients_helper.rb new file mode 100644 index 0000000..9015906 --- /dev/null +++ b/app/helpers/clients_helper.rb @@ -0,0 +1,2 @@ +module ClientsHelper +end diff --git a/app/models/api_key.rb b/app/models/api_key.rb new file mode 100644 index 0000000..c4bd78d --- /dev/null +++ b/app/models/api_key.rb @@ -0,0 +1,16 @@ +class ApiKey + include Mongoid::Document + field :access_token, type: String + + index({ access_token: 1}, { unique: true }) + + before_create :generate_access_token + +private + + def generate_access_token + begin + self.access_token = SecureRandom.hex + end + end +end diff --git a/app/models/client.rb b/app/models/client.rb new file mode 100644 index 0000000..f692a61 --- /dev/null +++ b/app/models/client.rb @@ -0,0 +1,9 @@ +class Client + include Mongoid::Document + field :site_name, type: String + field :site_token, type: String + field :site_id, type: String + + validates :site_id, :uniqueness => true + validates :site_token, :uniqueness => true +end diff --git a/app/models/user.rb b/app/models/user.rb index ec9b769..0c05947 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -4,7 +4,7 @@ class User # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, - :recoverable, :rememberable, :trackable, :validatable + :recoverable, :rememberable, :trackable, :validatable, :registerable ## Database authenticatable field :email, :type => String, :default => "" @@ -35,4 +35,10 @@ class User # field :unlock_token, :type => String # Only if unlock strategy is :email or :both # field :locked_at, :type => Time has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner + + def self.authenticate!(email, password) + user = User.where(email: email).first + return (user.valid_password?(password) ? user : nil) unless user.nil? + nil + end end diff --git a/app/views/clients/_form.html.erb b/app/views/clients/_form.html.erb new file mode 100644 index 0000000..e0fc796 --- /dev/null +++ b/app/views/clients/_form.html.erb @@ -0,0 +1,29 @@ +<%= form_for(@client) do |f| %> + <% if @client.errors.any? %> +
+

<%= pluralize(@client.errors.count, "error") %> prohibited this client from being saved:

+ + +
+ <% end %> + +
+ <%= f.label :site_name %>
+ <%= f.text_field :site_name %> +
+
+ <%= f.label :site_token %>
+ <%= f.text_field :site_token %> +
+
+ <%= f.label :site_id %>
+ <%= f.text_field :site_id %> +
+
+ <%= f.submit %> +
+<% end %> diff --git a/app/views/clients/edit.html.erb b/app/views/clients/edit.html.erb new file mode 100644 index 0000000..2667681 --- /dev/null +++ b/app/views/clients/edit.html.erb @@ -0,0 +1,6 @@ +

Editing client

+ +<%= render 'form' %> + +<%= link_to 'Show', @client %> | +<%= link_to 'Back', clients_path %> diff --git a/app/views/clients/index.html.erb b/app/views/clients/index.html.erb new file mode 100644 index 0000000..b9fb5c3 --- /dev/null +++ b/app/views/clients/index.html.erb @@ -0,0 +1,31 @@ +

Listing clients

+ + + + + + + + + + + + + + + <% @clients.each do |client| %> + + + + + + + + + <% end %> + +
Site nameSite tokenSite
<%= client.site_name %><%= client.site_token %><%= client.site_id %><%= link_to 'Show', client %><%= link_to 'Edit', edit_client_path(client) %><%= link_to 'Destroy', client, method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%= link_to 'New Client', new_client_path %> diff --git a/app/views/clients/index.json.jbuilder b/app/views/clients/index.json.jbuilder new file mode 100644 index 0000000..be160f2 --- /dev/null +++ b/app/views/clients/index.json.jbuilder @@ -0,0 +1,4 @@ +json.array!(@clients) do |client| + json.extract! client, :id, :site_name, :site_token, :site_id + json.url client_url(client, format: :json) +end diff --git a/app/views/clients/new.html.erb b/app/views/clients/new.html.erb new file mode 100644 index 0000000..623e4cf --- /dev/null +++ b/app/views/clients/new.html.erb @@ -0,0 +1,5 @@ +

New client

+ +<%= render 'form' %> + +<%= link_to 'Back', clients_path %> diff --git a/app/views/clients/show.html.erb b/app/views/clients/show.html.erb new file mode 100644 index 0000000..688f180 --- /dev/null +++ b/app/views/clients/show.html.erb @@ -0,0 +1,19 @@ +

<%= notice %>

+ +

+ Site name: + <%= @client.site_name %> +

+ +

+ Site token: + <%= @client.site_token %> +

+ +

+ Site: + <%= @client.site_id %> +

+ +<%= link_to 'Edit', edit_client_path(@client) %> | +<%= link_to 'Back', clients_path %> diff --git a/app/views/clients/show.json.jbuilder b/app/views/clients/show.json.jbuilder new file mode 100644 index 0000000..92b4dcb --- /dev/null +++ b/app/views/clients/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @client, :id, :site_name, :site_token, :site_id, :created_at, :updated_at diff --git a/config/application.rb b/config/application.rb index 53a836a..a07cc4c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -24,5 +24,8 @@ module Mtstore # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de + config.to_prepare do + DeviseController.respond_to :html, :json + end end end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 7bd9070..1d5f8be 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -3,11 +3,16 @@ Doorkeeper.configure do # Currently supported options are :active_record, :mongoid2, :mongoid3, :mongo_mapper orm :mongoid4 + resource_owner_authenticator do |routes| + current_user || warden.authenticate!(:scope => :user) + end + # This block will be called to check whether the resource owner is authenticated or not. resource_owner_from_credentials do |routes| request.params[:user] = {:email => request.params[:username], :password => request.params[:password]} request.env["devise.allow_params_authentication"] = true request.env["warden"].authenticate!(:scope => :user) + # User.authenticate!(params[:username], params[:password]) end # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. diff --git a/config/routes.rb b/config/routes.rb index 38d3fcb..55fab86 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,17 +2,19 @@ require 'api_constraints' Mtstore::Application.routes.draw do + resources :clients + # get "search/index" # get "search/show" - use_doorkeeper do - controllers :applications => 'oauth/applications' - end devise_for :users namespace :api, defaults: {format: 'json'} do scope module: :v1, constraints: ApiConstraints.new(version: 1, default: :true) do resources :templates resources :extensions + resources :clients do + post 'verify_client', on: :collection + end end end resources :templates diff --git a/rest_client.rb b/rest_client.rb new file mode 100644 index 0000000..e1d739c --- /dev/null +++ b/rest_client.rb @@ -0,0 +1,2 @@ +response = curl -i "http://localhost:3000/oauth/token" -F grant_type=password -F client_id='8dae7e34b1ba624e601cf659b65a70fa92d1c408d1f18252f9c0119b3efdce8d' -F client_secret='e11386baaa4cd9a2327ce3a170ec7ae74d88c5ed618342852492f7603e065cb9' -F username="orbit@rulingcom.com" -F password="bjo4xjp6" +puts response diff --git a/test/controllers/clients_controller_test.rb b/test/controllers/clients_controller_test.rb new file mode 100644 index 0000000..bdefa90 --- /dev/null +++ b/test/controllers/clients_controller_test.rb @@ -0,0 +1,49 @@ +require 'test_helper' + +class ClientsControllerTest < ActionController::TestCase + setup do + @client = clients(:one) + end + + test "should get index" do + get :index + assert_response :success + assert_not_nil assigns(:clients) + end + + test "should get new" do + get :new + assert_response :success + end + + test "should create client" do + assert_difference('Client.count') do + post :create, client: { site_id: @client.site_id, site_name: @client.site_name, site_token: @client.site_token } + end + + assert_redirected_to client_path(assigns(:client)) + end + + test "should show client" do + get :show, id: @client + assert_response :success + end + + test "should get edit" do + get :edit, id: @client + assert_response :success + end + + test "should update client" do + patch :update, id: @client, client: { site_id: @client.site_id, site_name: @client.site_name, site_token: @client.site_token } + assert_redirected_to client_path(assigns(:client)) + end + + test "should destroy client" do + assert_difference('Client.count', -1) do + delete :destroy, id: @client + end + + assert_redirected_to clients_path + end +end diff --git a/test/fixtures/api_keys.yml b/test/fixtures/api_keys.yml new file mode 100644 index 0000000..bbd2b2b --- /dev/null +++ b/test/fixtures/api_keys.yml @@ -0,0 +1,7 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + access_token: MyString + +two: + access_token: MyString diff --git a/test/fixtures/clients.yml b/test/fixtures/clients.yml new file mode 100644 index 0000000..d53a444 --- /dev/null +++ b/test/fixtures/clients.yml @@ -0,0 +1,11 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + site_name: MyString + site_token: MyString + site_id: MyString + +two: + site_name: MyString + site_token: MyString + site_id: MyString diff --git a/test/helpers/clients_helper_test.rb b/test/helpers/clients_helper_test.rb new file mode 100644 index 0000000..21f2d9b --- /dev/null +++ b/test/helpers/clients_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class ClientsHelperTest < ActionView::TestCase +end diff --git a/test/models/api_key_test.rb b/test/models/api_key_test.rb new file mode 100644 index 0000000..2b10127 --- /dev/null +++ b/test/models/api_key_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ApiKeyTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/client_test.rb b/test/models/client_test.rb new file mode 100644 index 0000000..2dfc9cc --- /dev/null +++ b/test/models/client_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ClientTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end