API Keys and Authorization Framework

This commit is contained in:
Saurabh Bhatia 2014-02-13 14:51:23 +08:00
parent 9819bc3cfd
commit 213e5c1780
33 changed files with 461 additions and 18 deletions

View File

@ -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'

View File

@ -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!

View File

@ -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/

View File

@ -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/

View File

@ -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
})

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
module ClientsHelper
end

16
app/models/api_key.rb Normal file
View File

@ -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

9
app/models/client.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -0,0 +1,29 @@
<%= form_for(@client) do |f| %>
<% if @client.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@client.errors.count, "error") %> prohibited this client from being saved:</h2>
<ul>
<% @client.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :site_name %><br>
<%= f.text_field :site_name %>
</div>
<div class="field">
<%= f.label :site_token %><br>
<%= f.text_field :site_token %>
</div>
<div class="field">
<%= f.label :site_id %><br>
<%= f.text_field :site_id %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>

View File

@ -0,0 +1,6 @@
<h1>Editing client</h1>
<%= render 'form' %>
<%= link_to 'Show', @client %> |
<%= link_to 'Back', clients_path %>

View File

@ -0,0 +1,31 @@
<h1>Listing clients</h1>
<table>
<thead>
<tr>
<th>Site name</th>
<th>Site token</th>
<th>Site</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<% @clients.each do |client| %>
<tr>
<td><%= client.site_name %></td>
<td><%= client.site_token %></td>
<td><%= client.site_id %></td>
<td><%= link_to 'Show', client %></td>
<td><%= link_to 'Edit', edit_client_path(client) %></td>
<td><%= link_to 'Destroy', client, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Client', new_client_path %>

View File

@ -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

View File

@ -0,0 +1,5 @@
<h1>New client</h1>
<%= render 'form' %>
<%= link_to 'Back', clients_path %>

View File

@ -0,0 +1,19 @@
<p id="notice"><%= notice %></p>
<p>
<strong>Site name:</strong>
<%= @client.site_name %>
</p>
<p>
<strong>Site token:</strong>
<%= @client.site_token %>
</p>
<p>
<strong>Site:</strong>
<%= @client.site_id %>
</p>
<%= link_to 'Edit', edit_client_path(@client) %> |
<%= link_to 'Back', clients_path %>

View File

@ -0,0 +1 @@
json.extract! @client, :id, :site_name, :site_token, :site_id, :created_at, :updated_at

View File

@ -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

View File

@ -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.

View File

@ -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

2
rest_client.rb Normal file
View File

@ -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

View File

@ -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

7
test/fixtures/api_keys.yml vendored Normal file
View File

@ -0,0 +1,7 @@
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
access_token: MyString
two:
access_token: MyString

11
test/fixtures/clients.yml vendored Normal file
View File

@ -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

View File

@ -0,0 +1,4 @@
require 'test_helper'
class ClientsHelperTest < ActionView::TestCase
end

View File

@ -0,0 +1,7 @@
require 'test_helper'
class ApiKeyTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View File

@ -0,0 +1,7 @@
require 'test_helper'
class ClientTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end