Doorkeeper integration
This commit is contained in:
parent
5cf2bd4c99
commit
e41dadcb33
36 changed files with 900 additions and 3 deletions
2
Gemfile
2
Gemfile
|
@ -29,6 +29,8 @@ gem 'omniauth-twitter'
|
|||
gem 'omniauth-github'
|
||||
gem 'omniauth-shibboleth'
|
||||
gem 'omniauth-kerberos'
|
||||
gem 'doorkeeper', '2.0.1'
|
||||
gem "rack-oauth2", "~> 1.0.5"
|
||||
|
||||
# Extracting information from a git repository
|
||||
# Provide access to Gitlab::Git library
|
||||
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -37,6 +37,7 @@ GEM
|
|||
rake (>= 0.8.7)
|
||||
arel (5.0.1.20140414130214)
|
||||
asciidoctor (0.1.4)
|
||||
attr_required (1.0.0)
|
||||
awesome_print (1.2.0)
|
||||
axiom-types (0.0.5)
|
||||
descendants_tracker (~> 0.0.1)
|
||||
|
@ -107,6 +108,8 @@ GEM
|
|||
diff-lcs (1.2.5)
|
||||
diffy (3.0.3)
|
||||
docile (1.1.5)
|
||||
doorkeeper (2.0.1)
|
||||
railties (>= 3.1)
|
||||
dotenv (0.9.0)
|
||||
dropzonejs-rails (0.4.14)
|
||||
rails (> 3.1)
|
||||
|
@ -250,6 +253,7 @@ GEM
|
|||
json (~> 1.8)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpauth (0.2.1)
|
||||
httpclient (2.5.3.3)
|
||||
i18n (0.6.11)
|
||||
ice_nine (0.10.0)
|
||||
jasmine (2.0.2)
|
||||
|
@ -368,6 +372,12 @@ GEM
|
|||
rack (>= 1.1.3)
|
||||
rack-mount (0.8.3)
|
||||
rack (>= 1.0.0)
|
||||
rack-oauth2 (1.0.8)
|
||||
activesupport (>= 2.3)
|
||||
attr_required (>= 0.0.5)
|
||||
httpclient (>= 2.2.0.2)
|
||||
multi_json (>= 1.3.6)
|
||||
rack (>= 1.1)
|
||||
rack-protection (1.5.1)
|
||||
rack
|
||||
rack-test (0.6.2)
|
||||
|
@ -616,6 +626,7 @@ DEPENDENCIES
|
|||
devise (= 3.2.4)
|
||||
devise-async (= 0.9.0)
|
||||
diffy (~> 3.0.3)
|
||||
doorkeeper (= 2.0.1)
|
||||
dropzonejs-rails
|
||||
email_spec
|
||||
enumerize
|
||||
|
@ -672,6 +683,7 @@ DEPENDENCIES
|
|||
rack-attack
|
||||
rack-cors
|
||||
rack-mini-profiler
|
||||
rack-oauth2 (~> 1.0.5)
|
||||
rails (~> 4.1.0)
|
||||
rails_autolink (~> 1.1)
|
||||
rails_best_practices
|
||||
|
|
25
app/controllers/oauth/applications_controller.rb
Normal file
25
app/controllers/oauth/applications_controller.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
|
||||
before_filter :authenticate_user!
|
||||
layout "profile"
|
||||
|
||||
def index
|
||||
@applications = current_user.oauth_applications
|
||||
end
|
||||
|
||||
def create
|
||||
@application = Doorkeeper::Application.new(application_params)
|
||||
@application.owner = current_user if Doorkeeper.configuration.confirm_application_owner?
|
||||
if @application.save
|
||||
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
|
||||
redirect_to oauth_application_url(@application)
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :destroy]) if @application.destroy
|
||||
redirect_to profile_account_url
|
||||
end
|
||||
|
||||
end
|
57
app/controllers/oauth/authorizations_controller.rb
Normal file
57
app/controllers/oauth/authorizations_controller.rb
Normal file
|
@ -0,0 +1,57 @@
|
|||
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||
before_filter :authenticate_resource_owner!
|
||||
layout "profile"
|
||||
|
||||
def new
|
||||
if pre_auth.authorizable?
|
||||
if skip_authorization? || matching_token?
|
||||
auth = authorization.authorize
|
||||
redirect_to auth.redirect_uri
|
||||
else
|
||||
render "doorkeeper/authorizations/new"
|
||||
end
|
||||
else
|
||||
render "doorkeeper/authorizations/error"
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Handle raise invalid authorization
|
||||
def create
|
||||
redirect_or_render authorization.authorize
|
||||
end
|
||||
|
||||
def destroy
|
||||
redirect_or_render authorization.deny
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def matching_token?
|
||||
Doorkeeper::AccessToken.matching_token_for pre_auth.client,
|
||||
current_resource_owner.id,
|
||||
pre_auth.scopes
|
||||
end
|
||||
|
||||
def redirect_or_render(auth)
|
||||
if auth.redirectable?
|
||||
redirect_to auth.redirect_uri
|
||||
else
|
||||
render json: auth.body, status: auth.status
|
||||
end
|
||||
end
|
||||
|
||||
def pre_auth
|
||||
@pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new(Doorkeeper.configuration,
|
||||
server.client_via_uid,
|
||||
params)
|
||||
end
|
||||
|
||||
def authorization
|
||||
@authorization ||= strategy.request
|
||||
end
|
||||
|
||||
def strategy
|
||||
@strategy ||= server.authorization_request pre_auth.response_type
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
|
||||
layout "profile"
|
||||
|
||||
def destroy
|
||||
Doorkeeper::AccessToken.revoke_all_for params[:id], current_resource_owner
|
||||
redirect_to profile_account_url, notice: I18n.t(:notice, scope: [:doorkeeper, :flash, :authorized_applications, :destroy])
|
||||
end
|
||||
end
|
|
@ -3,5 +3,7 @@ class Profiles::AccountsController < ApplicationController
|
|||
|
||||
def show
|
||||
@user = current_user
|
||||
@applications = current_user.oauth_applications
|
||||
@authorized_applications = Doorkeeper::Application.authorized_for(current_user)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -106,6 +106,7 @@ class User < ActiveRecord::Base
|
|||
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
|
||||
has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
|
||||
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
|
||||
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
|
||||
|
||||
|
||||
#
|
||||
|
|
41
app/services/oauth2/access_token_validation_service.rb
Normal file
41
app/services/oauth2/access_token_validation_service.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
module Oauth2::AccessTokenValidationService
|
||||
# Results:
|
||||
VALID = :valid
|
||||
EXPIRED = :expired
|
||||
REVOKED = :revoked
|
||||
INSUFFICIENT_SCOPE = :insufficient_scope
|
||||
|
||||
class << self
|
||||
def validate(token, scopes: [])
|
||||
if token.expired?
|
||||
return EXPIRED
|
||||
|
||||
elsif token.revoked?
|
||||
return REVOKED
|
||||
|
||||
elsif !self.sufficent_scope?(token, scopes)
|
||||
return INSUFFICIENT_SCOPE
|
||||
|
||||
else
|
||||
return VALID
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# True if the token's scope is a superset of required scopes,
|
||||
# or the required scopes is empty.
|
||||
def sufficent_scope?(token, scopes)
|
||||
if scopes.blank?
|
||||
# if no any scopes required, the scopes of token is sufficient.
|
||||
return true
|
||||
else
|
||||
# If there are scopes required, then check whether
|
||||
# the set of authorized scopes is a superset of the set of required scopes
|
||||
required_scopes = Set.new(scopes)
|
||||
authorized_scopes = Set.new(token.scopes)
|
||||
|
||||
return authorized_scopes >= required_scopes
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
4
app/views/doorkeeper/applications/_delete_form.html.haml
Normal file
4
app/views/doorkeeper/applications/_delete_form.html.haml
Normal file
|
@ -0,0 +1,4 @@
|
|||
- submit_btn_css ||= 'btn btn-link btn-remove btn-small'
|
||||
= form_tag oauth_application_path(application) do
|
||||
%input{:name => "_method", :type => "hidden", :value => "delete"}/
|
||||
= submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
|
25
app/views/doorkeeper/applications/_form.html.haml
Normal file
25
app/views/doorkeeper/applications/_form.html.haml
Normal file
|
@ -0,0 +1,25 @@
|
|||
= form_for application, url: doorkeeper_submit_path(application), html: {class: 'form-horizontal', role: 'form'} do |f|
|
||||
- if application.errors.any?
|
||||
.alert.alert-danger{"data-alert" => ""}
|
||||
%p Whoops! Check your form for possible errors
|
||||
= content_tag :div, class: "form-group#{' has-error' if application.errors[:name].present?}" do
|
||||
= f.label :name, class: 'col-sm-2 control-label'
|
||||
.col-sm-10
|
||||
= f.text_field :name, class: 'form-control'
|
||||
= doorkeeper_errors_for application, :name
|
||||
= content_tag :div, class: "form-group#{' has-error' if application.errors[:redirect_uri].present?}" do
|
||||
= f.label :redirect_uri, class: 'col-sm-2 control-label'
|
||||
.col-sm-10
|
||||
= f.text_area :redirect_uri, class: 'form-control'
|
||||
= doorkeeper_errors_for application, :redirect_uri
|
||||
%span.help-block
|
||||
Use one line per URI
|
||||
- if Doorkeeper.configuration.native_redirect_uri
|
||||
%span.help-block
|
||||
Use
|
||||
%code= Doorkeeper.configuration.native_redirect_uri
|
||||
for local tests
|
||||
.form-group
|
||||
.col-sm-offset-2.col-sm-10
|
||||
= f.submit 'Submit', class: "btn btn-primary wide"
|
||||
= link_to "Cancel", profile_account_path, :class => "btn btn-default"
|
2
app/views/doorkeeper/applications/edit.html.haml
Normal file
2
app/views/doorkeeper/applications/edit.html.haml
Normal file
|
@ -0,0 +1,2 @@
|
|||
%h3.page-title Edit application
|
||||
= render 'form', application: @application
|
16
app/views/doorkeeper/applications/index.html.haml
Normal file
16
app/views/doorkeeper/applications/index.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
|||
%h3.page-title Your applications
|
||||
%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Name
|
||||
%th Callback URL
|
||||
%th
|
||||
%th
|
||||
%tbody
|
||||
- @applications.each do |application|
|
||||
%tr{:id => "application_#{application.id}"}
|
||||
%td= link_to application.name, oauth_application_path(application)
|
||||
%td= application.redirect_uri
|
||||
%td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link'
|
||||
%td= render 'delete_form', application: application
|
2
app/views/doorkeeper/applications/new.html.haml
Normal file
2
app/views/doorkeeper/applications/new.html.haml
Normal file
|
@ -0,0 +1,2 @@
|
|||
%h3.page-title New application
|
||||
= render 'form', application: @application
|
21
app/views/doorkeeper/applications/show.html.haml
Normal file
21
app/views/doorkeeper/applications/show.html.haml
Normal file
|
@ -0,0 +1,21 @@
|
|||
%h3.page-title
|
||||
Application: #{@application.name}
|
||||
.row
|
||||
.col-md-8
|
||||
%h4 Application Id:
|
||||
%p
|
||||
%code#application_id= @application.uid
|
||||
%h4 Secret:
|
||||
%p
|
||||
%code#secret= @application.secret
|
||||
%h4 Callback urls:
|
||||
%table
|
||||
- @application.redirect_uri.split.each do |uri|
|
||||
%tr
|
||||
%td
|
||||
%code= uri
|
||||
%td
|
||||
= link_to 'Authorize', oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: 'code'), class: 'btn btn-success', target: '_blank'
|
||||
.prepend-top-20
|
||||
%p= link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left'
|
||||
%p= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
|
3
app/views/doorkeeper/authorizations/error.html.haml
Normal file
3
app/views/doorkeeper/authorizations/error.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
%h3.page-title An error has occurred
|
||||
%main{:role => "main"}
|
||||
%pre= @pre_auth.error_response.body[:error_description]
|
28
app/views/doorkeeper/authorizations/new.html.haml
Normal file
28
app/views/doorkeeper/authorizations/new.html.haml
Normal file
|
@ -0,0 +1,28 @@
|
|||
%h3.page-title Authorize required
|
||||
%main{:role => "main"}
|
||||
%p.h4
|
||||
Authorize
|
||||
%strong.text-info= @pre_auth.client.name
|
||||
to use your account?
|
||||
- if @pre_auth.scopes
|
||||
#oauth-permissions
|
||||
%p This application will be able to:
|
||||
%ul.text-info
|
||||
- @pre_auth.scopes.each do |scope|
|
||||
%li= t scope, scope: [:doorkeeper, :scopes]
|
||||
%hr/
|
||||
.actions
|
||||
= form_tag oauth_authorization_path, method: :post do
|
||||
= hidden_field_tag :client_id, @pre_auth.client.uid
|
||||
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
|
||||
= hidden_field_tag :state, @pre_auth.state
|
||||
= hidden_field_tag :response_type, @pre_auth.response_type
|
||||
= hidden_field_tag :scope, @pre_auth.scope
|
||||
= submit_tag "Authorize", class: "btn btn-success wide pull-left"
|
||||
= form_tag oauth_authorization_path, method: :delete do
|
||||
= hidden_field_tag :client_id, @pre_auth.client.uid
|
||||
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
|
||||
= hidden_field_tag :state, @pre_auth.state
|
||||
= hidden_field_tag :response_type, @pre_auth.response_type
|
||||
= hidden_field_tag :scope, @pre_auth.scope
|
||||
= submit_tag "Deny", class: "btn btn-danger prepend-left-10"
|
3
app/views/doorkeeper/authorizations/show.html.haml
Normal file
3
app/views/doorkeeper/authorizations/show.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
%h3.page-title Authorization code:
|
||||
%main{:role => "main"}
|
||||
%code#authorization_code= params[:code]
|
|
@ -0,0 +1,4 @@
|
|||
- submit_btn_css ||= 'btn btn-link btn-remove'
|
||||
= form_tag oauth_authorized_application_path(application) do
|
||||
%input{:name => "_method", :type => "hidden", :value => "delete"}/
|
||||
= submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-small'
|
16
app/views/doorkeeper/authorized_applications/index.html.haml
Normal file
16
app/views/doorkeeper/authorized_applications/index.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
|||
%header.page-header
|
||||
%h1 Your authorized applications
|
||||
%main{:role => "main"}
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Application
|
||||
%th Created At
|
||||
%th
|
||||
%th
|
||||
%tbody
|
||||
- @applications.each do |application|
|
||||
%tr
|
||||
%td= application.name
|
||||
%td= application.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
%td= render 'delete_form', application: application
|
34
app/views/layouts/doorkeeper/admin.html.erb
Normal file
34
app/views/layouts/doorkeeper/admin.html.erb
Normal file
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Doorkeeper</title>
|
||||
<%= stylesheet_link_tag "doorkeeper/admin/application" %>
|
||||
<%= csrf_meta_tags %>
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<%= link_to 'OAuth2 Provider', oauth_applications_path, class: 'navbar-brand' %>
|
||||
</div>
|
||||
<ul class="nav navbar-nav">
|
||||
<%= content_tag :li, class: "#{'active' if request.path == oauth_applications_path}" do %>
|
||||
<%= link_to 'Applications', oauth_applications_path %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<%- if flash[:notice].present? %>
|
||||
<div class="alert alert-info">
|
||||
<%= flash[:notice] %>
|
||||
</div>
|
||||
<% end -%>
|
||||
|
||||
<%= yield %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
23
app/views/layouts/doorkeeper/application.html.erb
Normal file
23
app/views/layouts/doorkeeper/application.html.erb
Normal file
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OAuth authorize required</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<%= stylesheet_link_tag "doorkeeper/application" %>
|
||||
<%= csrf_meta_tags %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<%- if flash[:notice].present? %>
|
||||
<div class="alert alert-info">
|
||||
<%= flash[:notice] %>
|
||||
</div>
|
||||
<% end -%>
|
||||
|
||||
<%= yield %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -3,7 +3,7 @@
|
|||
= link_to profile_path, title: "Profile" do
|
||||
%i.fa.fa-user
|
||||
Profile
|
||||
= nav_link(controller: :accounts) do
|
||||
= nav_link(controller: [:accounts, :applications]) do
|
||||
= link_to profile_account_path do
|
||||
%i.fa.fa-gear
|
||||
Account
|
||||
|
|
|
@ -75,3 +75,38 @@
|
|||
The following groups will be abandoned. You should transfer or remove them:
|
||||
%strong #{current_user.solo_owned_groups.map(&:name).join(', ')}
|
||||
= link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
|
||||
|
||||
%h3.page-title
|
||||
OAuth2
|
||||
%fieldset.oauth-applications
|
||||
%legend Your applications
|
||||
%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Name
|
||||
%th Callback URL
|
||||
%th
|
||||
%th
|
||||
%tbody
|
||||
- @applications.each do |application|
|
||||
%tr{:id => "application_#{application.id}"}
|
||||
%td= link_to application.name, oauth_application_path(application)
|
||||
%td= application.redirect_uri
|
||||
%td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-small'
|
||||
%td= render 'doorkeeper/applications/delete_form', application: application
|
||||
|
||||
%fieldset.oauth-authorized-applications
|
||||
%legend Your authorized applications
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Name
|
||||
%th Created At
|
||||
%th
|
||||
%tbody
|
||||
- @authorized_applications.each do |application|
|
||||
%tr{:id => "application_#{application.id}"}
|
||||
%td= link_to application.name, oauth_application_path(application)
|
||||
%td= application.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
%td= render 'doorkeeper/authorized_applications/delete_form', application: application
|
||||
|
|
91
config/initializers/doorkeeper.rb
Normal file
91
config/initializers/doorkeeper.rb
Normal file
|
@ -0,0 +1,91 @@
|
|||
Doorkeeper.configure do
|
||||
# Change the ORM that doorkeeper will use.
|
||||
# Currently supported options are :active_record, :mongoid2, :mongoid3, :mongo_mapper
|
||||
orm :active_record
|
||||
|
||||
# This block will be called to check whether the resource owner is authenticated or not.
|
||||
resource_owner_authenticator do
|
||||
# Put your resource owner authentication logic here.
|
||||
# Example implementation:
|
||||
current_user || 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
|
||||
|
||||
# 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 => true
|
||||
|
||||
# Define access token scopes for your provider
|
||||
# For more information go to
|
||||
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
|
||||
default_scopes :api
|
||||
#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_access_token_param, :from_bearer_authorization, :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 nil#'urn:ietf:wg:oauth:2.0:oob'
|
||||
|
||||
# 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 all the four grant flows.
|
||||
#
|
||||
# grant_flows %w(authorization_code implicit password 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 trusted a application.
|
||||
# skip_authorization do |resource_owner, client|
|
||||
# client.superapp? or resource_owner.admin?
|
||||
# end
|
||||
|
||||
# WWW-Authenticate Realm (default "Doorkeeper").
|
||||
# realm "Doorkeeper"
|
||||
|
||||
# Allow dynamic query parameters (disabled by default)
|
||||
# Some applications require dynamic query parameters on their request_uri
|
||||
# set to true if you want this to be allowed
|
||||
# wildcard_redirect_uri false
|
||||
end
|
73
config/locales/doorkeeper.en.yml
Normal file
73
config/locales/doorkeeper.en.yml
Normal file
|
@ -0,0 +1,73 @@
|
|||
en:
|
||||
activerecord:
|
||||
errors:
|
||||
models:
|
||||
application:
|
||||
attributes:
|
||||
redirect_uri:
|
||||
fragment_present: 'cannot contain a fragment.'
|
||||
invalid_uri: 'must be a valid URI.'
|
||||
relative_uri: 'must be an absolute URI.'
|
||||
mongoid:
|
||||
errors:
|
||||
models:
|
||||
application:
|
||||
attributes:
|
||||
redirect_uri:
|
||||
fragment_present: 'cannot contain a fragment.'
|
||||
invalid_uri: 'must be a valid URI.'
|
||||
relative_uri: 'must be an absolute URI.'
|
||||
mongo_mapper:
|
||||
errors:
|
||||
models:
|
||||
application:
|
||||
attributes:
|
||||
redirect_uri:
|
||||
fragment_present: 'cannot contain a fragment.'
|
||||
invalid_uri: 'must be a valid URI.'
|
||||
relative_uri: 'must be an absolute URI.'
|
||||
doorkeeper:
|
||||
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"
|
||||
scopes:
|
||||
api: Access your API
|
||||
|
||||
flash:
|
||||
applications:
|
||||
create:
|
||||
notice: 'Application created.'
|
||||
destroy:
|
||||
notice: 'Application deleted.'
|
||||
update:
|
||||
notice: 'Application updated.'
|
||||
authorized_applications:
|
||||
destroy:
|
||||
notice: 'Application revoked.'
|
|
@ -2,6 +2,11 @@ require 'sidekiq/web'
|
|||
require 'api/api'
|
||||
|
||||
Gitlab::Application.routes.draw do
|
||||
use_doorkeeper do
|
||||
controllers :applications => 'oauth/applications',
|
||||
:authorized_applications => 'oauth/authorized_applications',
|
||||
:authorizations => 'oauth/authorizations'
|
||||
end
|
||||
#
|
||||
# Search
|
||||
#
|
||||
|
|
42
db/migrate/20141216155758_create_doorkeeper_tables.rb
Normal file
42
db/migrate/20141216155758_create_doorkeeper_tables.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
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
|
||||
end
|
||||
|
||||
add_index :oauth_applications, :uid, unique: true
|
||||
|
||||
create_table :oauth_access_grants do |t|
|
||||
t.integer :resource_owner_id, null: false
|
||||
t.integer :application_id, 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
|
||||
|
||||
create_table :oauth_access_tokens do |t|
|
||||
t.integer :resource_owner_id
|
||||
t.integer :application_id
|
||||
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
|
||||
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
|
||||
end
|
||||
end
|
7
db/migrate/20141217125223_add_owner_to_application.rb
Normal file
7
db/migrate/20141217125223_add_owner_to_application.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class AddOwnerToApplication < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :oauth_applications, :owner_id, :integer, null: true
|
||||
add_column :oauth_applications, :owner_type, :string, null: true
|
||||
add_index :oauth_applications, [:owner_id, :owner_type]
|
||||
end
|
||||
end
|
45
db/schema.rb
45
db/schema.rb
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20141205134006) do
|
||||
ActiveRecord::Schema.define(version: 20141217125223) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -249,6 +249,49 @@ ActiveRecord::Schema.define(version: 20141205134006) do
|
|||
add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
|
||||
add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
|
||||
|
||||
create_table "oauth_access_grants", force: true do |t|
|
||||
t.integer "resource_owner_id", null: false
|
||||
t.integer "application_id", 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"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree
|
||||
|
||||
create_table "oauth_access_tokens", force: true do |t|
|
||||
t.integer "resource_owner_id"
|
||||
t.integer "application_id"
|
||||
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"
|
||||
end
|
||||
|
||||
add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree
|
||||
add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree
|
||||
add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree
|
||||
|
||||
create_table "oauth_applications", force: true 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", default: "", null: false
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.integer "owner_id"
|
||||
t.string "owner_type"
|
||||
end
|
||||
|
||||
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
|
||||
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
|
||||
|
||||
create_table "projects", force: true do |t|
|
||||
t.string "name"
|
||||
t.string "path"
|
||||
|
|
|
@ -71,6 +71,20 @@ Feature: Profile
|
|||
And I click on my profile picture
|
||||
Then I should see my user page
|
||||
|
||||
Scenario: I can manage application
|
||||
Given I visit profile account page
|
||||
Then I click on new application button
|
||||
And I should see application form
|
||||
Then I fill application form out and submit
|
||||
And I see application
|
||||
Then I click edit
|
||||
And I see edit application form
|
||||
Then I change name of application and submit
|
||||
And I see that application was changed
|
||||
Then I visit profile account page
|
||||
And I click to remove application
|
||||
Then I see that application is removed
|
||||
|
||||
@javascript
|
||||
Scenario: I change my application theme
|
||||
Given I visit profile design page
|
||||
|
|
|
@ -221,4 +221,54 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
|
|||
step 'I should see groups I belong to' do
|
||||
page.should have_css('.profile-groups-avatars', visible: true)
|
||||
end
|
||||
|
||||
step 'I click on new application button' do
|
||||
click_on 'New Application'
|
||||
end
|
||||
|
||||
step 'I should see application form' do
|
||||
page.should have_content "New application"
|
||||
end
|
||||
|
||||
step 'I fill application form out and submit' do
|
||||
fill_in :doorkeeper_application_name, with: 'test'
|
||||
fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
|
||||
click_on "Submit"
|
||||
end
|
||||
|
||||
step 'I see application' do
|
||||
page.should have_content "Application: test"
|
||||
page.should have_content "Application Id"
|
||||
page.should have_content "Secret"
|
||||
end
|
||||
|
||||
step 'I click edit' do
|
||||
click_on "Edit"
|
||||
end
|
||||
|
||||
step 'I see edit application form' do
|
||||
page.should have_content "Edit application"
|
||||
end
|
||||
|
||||
step 'I change name of application and submit' do
|
||||
page.should have_content "Edit application"
|
||||
fill_in :doorkeeper_application_name, with: 'test_changed'
|
||||
click_on "Submit"
|
||||
end
|
||||
|
||||
step 'I see that application was changed' do
|
||||
page.should have_content "test_changed"
|
||||
page.should have_content "Application Id"
|
||||
page.should have_content "Secret"
|
||||
end
|
||||
|
||||
step 'I click to remove application' do
|
||||
within '.oauth-applications' do
|
||||
click_on "Destroy"
|
||||
end
|
||||
end
|
||||
|
||||
step "I see that application is removed" do
|
||||
page.find(".oauth-applications").should_not have_content "test_changed"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file}
|
|||
|
||||
module API
|
||||
class API < Grape::API
|
||||
include APIGuard
|
||||
version 'v3', using: :path
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound do
|
||||
|
|
175
lib/api/api_guard.rb
Normal file
175
lib/api/api_guard.rb
Normal file
|
@ -0,0 +1,175 @@
|
|||
# Guard API with OAuth 2.0 Access Token
|
||||
|
||||
require 'rack/oauth2'
|
||||
|
||||
module APIGuard
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do |base|
|
||||
# OAuth2 Resource Server Authentication
|
||||
use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request|
|
||||
# The authenticator only fetches the raw token string
|
||||
|
||||
# Must yield access token to store it in the env
|
||||
request.access_token
|
||||
end
|
||||
|
||||
helpers HelperMethods
|
||||
|
||||
install_error_responders(base)
|
||||
end
|
||||
|
||||
# Helper Methods for Grape Endpoint
|
||||
module HelperMethods
|
||||
# Invokes the doorkeeper guard.
|
||||
#
|
||||
# If token is presented and valid, then it sets @current_user.
|
||||
#
|
||||
# If the token does not have sufficient scopes to cover the requred scopes,
|
||||
# then it raises InsufficientScopeError.
|
||||
#
|
||||
# If the token is expired, then it raises ExpiredError.
|
||||
#
|
||||
# If the token is revoked, then it raises RevokedError.
|
||||
#
|
||||
# If the token is not found (nil), then it raises TokenNotFoundError.
|
||||
#
|
||||
# Arguments:
|
||||
#
|
||||
# scopes: (optional) scopes required for this guard.
|
||||
# Defaults to empty array.
|
||||
#
|
||||
def doorkeeper_guard!(scopes: [])
|
||||
if (access_token = find_access_token).nil?
|
||||
raise TokenNotFoundError
|
||||
|
||||
else
|
||||
case validate_access_token(access_token, scopes)
|
||||
when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
|
||||
raise InsufficientScopeError.new(scopes)
|
||||
|
||||
when Oauth2::AccessTokenValidationService::EXPIRED
|
||||
raise ExpiredError
|
||||
|
||||
when Oauth2::AccessTokenValidationService::REVOKED
|
||||
raise RevokedError
|
||||
|
||||
when Oauth2::AccessTokenValidationService::VALID
|
||||
@current_user = User.find(access_token.resource_owner_id)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def doorkeeper_guard(scopes: [])
|
||||
if access_token = find_access_token
|
||||
case validate_access_token(access_token, scopes)
|
||||
when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
|
||||
raise InsufficientScopeError.new(scopes)
|
||||
|
||||
when Oauth2::AccessTokenValidationService::EXPIRED
|
||||
raise ExpiredError
|
||||
|
||||
when Oauth2::AccessTokenValidationService::REVOKED
|
||||
raise RevokedError
|
||||
|
||||
when Oauth2::AccessTokenValidationService::VALID
|
||||
@current_user = User.find(access_token.resource_owner_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def current_user
|
||||
@current_user
|
||||
end
|
||||
|
||||
private
|
||||
def find_access_token
|
||||
@access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods)
|
||||
end
|
||||
|
||||
def doorkeeper_request
|
||||
@doorkeeper_request ||= ActionDispatch::Request.new(env)
|
||||
end
|
||||
|
||||
def validate_access_token(access_token, scopes)
|
||||
Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes)
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Installs the doorkeeper guard on the whole Grape API endpoint.
|
||||
#
|
||||
# Arguments:
|
||||
#
|
||||
# scopes: (optional) scopes required for this guard.
|
||||
# Defaults to empty array.
|
||||
#
|
||||
def guard_all!(scopes: [])
|
||||
before do
|
||||
guard! scopes: scopes
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def install_error_responders(base)
|
||||
error_classes = [ MissingTokenError, TokenNotFoundError,
|
||||
ExpiredError, RevokedError, InsufficientScopeError]
|
||||
|
||||
base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler
|
||||
end
|
||||
|
||||
def oauth2_bearer_token_error_handler
|
||||
Proc.new {|e|
|
||||
response = case e
|
||||
when MissingTokenError
|
||||
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new
|
||||
|
||||
when TokenNotFoundError
|
||||
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
|
||||
:invalid_token,
|
||||
"Bad Access Token.")
|
||||
|
||||
when ExpiredError
|
||||
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
|
||||
:invalid_token,
|
||||
"Token is expired. You can either do re-authorization or token refresh.")
|
||||
|
||||
when RevokedError
|
||||
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
|
||||
:invalid_token,
|
||||
"Token was revoked. You have to re-authorize from the user.")
|
||||
|
||||
when InsufficientScopeError
|
||||
# FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2)
|
||||
# does not include WWW-Authenticate header, which breaks the standard.
|
||||
Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(
|
||||
:insufficient_scope,
|
||||
Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope],
|
||||
{ :scope => e.scopes})
|
||||
end
|
||||
|
||||
response.finish
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Exceptions
|
||||
#
|
||||
|
||||
class MissingTokenError < StandardError; end
|
||||
|
||||
class TokenNotFoundError < StandardError; end
|
||||
|
||||
class ExpiredError < StandardError; end
|
||||
|
||||
class RevokedError < StandardError; end
|
||||
|
||||
class InsufficientScopeError < StandardError
|
||||
attr_reader :scopes
|
||||
def initialize(scopes)
|
||||
@scopes = scopes
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@ module API
|
|||
|
||||
def current_user
|
||||
private_token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
|
||||
@current_user ||= User.find_by(authentication_token: private_token)
|
||||
@current_user ||= (User.find_by(authentication_token: private_token) || doorkeeper_guard)
|
||||
|
||||
unless @current_user && Gitlab::UserAccess.allowed?(@current_user)
|
||||
return nil
|
||||
|
|
|
@ -41,6 +41,7 @@ describe API, api: true do
|
|||
describe ".current_user" do
|
||||
it "should return nil for an invalid token" do
|
||||
env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
|
||||
self.class.any_instance.stub(:doorkeeper_guard){ false }
|
||||
current_user.should be_nil
|
||||
end
|
||||
|
||||
|
|
31
spec/requests/api/doorkeeper_access_spec.rb
Normal file
31
spec/requests/api/doorkeeper_access_spec.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe API::API, api: true do
|
||||
include ApiHelpers
|
||||
|
||||
let!(:user) { create(:user) }
|
||||
let!(:application) { Doorkeeper::Application.create!(:name => "MyApp", :redirect_uri => "https://app.com", :owner => user) }
|
||||
let!(:token) { Doorkeeper::AccessToken.create! :application_id => application.id, :resource_owner_id => user.id }
|
||||
|
||||
|
||||
describe "when unauthenticated" do
|
||||
it "returns authentication success" do
|
||||
get api("/user"), :access_token => token.token
|
||||
response.status.should == 200
|
||||
end
|
||||
end
|
||||
|
||||
describe "when token invalid" do
|
||||
it "returns authentication error" do
|
||||
get api("/user"), :access_token => "123a"
|
||||
response.status.should == 401
|
||||
end
|
||||
end
|
||||
|
||||
describe "authorization by private token" do
|
||||
it "returns authentication success" do
|
||||
get api("/user", user)
|
||||
response.status.should == 200
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue