Merge branch 'siemens/gitlab-ce-feature/openid-connect'
This commit is contained in:
commit
de37dcee90
36 changed files with 731 additions and 131 deletions
1
Gemfile
1
Gemfile
|
@ -20,6 +20,7 @@ gem 'rugged', '~> 0.24.0'
|
||||||
# Authentication libraries
|
# Authentication libraries
|
||||||
gem 'devise', '~> 4.2'
|
gem 'devise', '~> 4.2'
|
||||||
gem 'doorkeeper', '~> 4.2.0'
|
gem 'doorkeeper', '~> 4.2.0'
|
||||||
|
gem 'doorkeeper-openid_connect', '~> 1.1.0'
|
||||||
gem 'omniauth', '~> 1.4.2'
|
gem 'omniauth', '~> 1.4.2'
|
||||||
gem 'omniauth-auth0', '~> 1.4.1'
|
gem 'omniauth-auth0', '~> 1.4.1'
|
||||||
gem 'omniauth-azure-oauth2', '~> 0.0.6'
|
gem 'omniauth-azure-oauth2', '~> 0.0.6'
|
||||||
|
|
13
Gemfile.lock
13
Gemfile.lock
|
@ -78,6 +78,7 @@ GEM
|
||||||
better_errors (1.0.1)
|
better_errors (1.0.1)
|
||||||
coderay (>= 1.0.0)
|
coderay (>= 1.0.0)
|
||||||
erubis (>= 2.6.6)
|
erubis (>= 2.6.6)
|
||||||
|
bindata (2.3.5)
|
||||||
binding_of_caller (0.7.2)
|
binding_of_caller (0.7.2)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
bootstrap-sass (3.3.6)
|
bootstrap-sass (3.3.6)
|
||||||
|
@ -167,6 +168,9 @@ GEM
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
doorkeeper (4.2.0)
|
doorkeeper (4.2.0)
|
||||||
railties (>= 4.2)
|
railties (>= 4.2)
|
||||||
|
doorkeeper-openid_connect (1.1.2)
|
||||||
|
doorkeeper (~> 4.0)
|
||||||
|
json-jwt (~> 1.6)
|
||||||
dropzonejs-rails (0.7.2)
|
dropzonejs-rails (0.7.2)
|
||||||
rails (> 3.1)
|
rails (> 3.1)
|
||||||
email_reply_trimmer (0.1.6)
|
email_reply_trimmer (0.1.6)
|
||||||
|
@ -376,6 +380,12 @@ GEM
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
thor (>= 0.14, < 2.0)
|
thor (>= 0.14, < 2.0)
|
||||||
json (1.8.6)
|
json (1.8.6)
|
||||||
|
json-jwt (1.7.1)
|
||||||
|
activesupport
|
||||||
|
bindata
|
||||||
|
multi_json (>= 1.3)
|
||||||
|
securecompare
|
||||||
|
url_safe_base64
|
||||||
json-schema (2.6.2)
|
json-schema (2.6.2)
|
||||||
addressable (~> 2.3.8)
|
addressable (~> 2.3.8)
|
||||||
jwt (1.5.6)
|
jwt (1.5.6)
|
||||||
|
@ -684,6 +694,7 @@ GEM
|
||||||
scss_lint (0.47.1)
|
scss_lint (0.47.1)
|
||||||
rake (>= 0.9, < 11)
|
rake (>= 0.9, < 11)
|
||||||
sass (~> 3.4.15)
|
sass (~> 3.4.15)
|
||||||
|
securecompare (1.0.0)
|
||||||
seed-fu (2.3.6)
|
seed-fu (2.3.6)
|
||||||
activerecord (>= 3.1)
|
activerecord (>= 3.1)
|
||||||
activesupport (>= 3.1)
|
activesupport (>= 3.1)
|
||||||
|
@ -789,6 +800,7 @@ GEM
|
||||||
get_process_mem (~> 0)
|
get_process_mem (~> 0)
|
||||||
unicorn (>= 4, < 6)
|
unicorn (>= 4, < 6)
|
||||||
uniform_notifier (1.10.0)
|
uniform_notifier (1.10.0)
|
||||||
|
url_safe_base64 (0.2.2)
|
||||||
validates_hostname (1.0.6)
|
validates_hostname (1.0.6)
|
||||||
activerecord (>= 3.0)
|
activerecord (>= 3.0)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
|
@ -866,6 +878,7 @@ DEPENDENCIES
|
||||||
devise-two-factor (~> 3.0.0)
|
devise-two-factor (~> 3.0.0)
|
||||||
diffy (~> 3.1.0)
|
diffy (~> 3.1.0)
|
||||||
doorkeeper (~> 4.2.0)
|
doorkeeper (~> 4.2.0)
|
||||||
|
doorkeeper-openid_connect (~> 1.1.0)
|
||||||
dropzonejs-rails (~> 0.7.1)
|
dropzonejs-rails (~> 0.7.1)
|
||||||
email_reply_trimmer (~> 0.1)
|
email_reply_trimmer (~> 0.1)
|
||||||
email_spec (~> 1.6.0)
|
email_spec (~> 1.6.0)
|
||||||
|
|
|
@ -2,7 +2,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
|
||||||
include OauthApplications
|
include OauthApplications
|
||||||
|
|
||||||
before_action :set_application, only: [:show, :edit, :update, :destroy]
|
before_action :set_application, only: [:show, :edit, :update, :destroy]
|
||||||
before_action :load_scopes, only: [:new, :edit]
|
before_action :load_scopes, only: [:new, :create, :edit, :update]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@applications = Doorkeeper::Application.where("owner_id IS NULL")
|
@applications = Doorkeeper::Application.where("owner_id IS NULL")
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||||
before_action :authenticate_resource_owner!
|
|
||||||
|
|
||||||
layout 'profile'
|
layout 'profile'
|
||||||
|
|
||||||
|
# Overriden from Doorkeeper::AuthorizationsController to
|
||||||
|
# include the call to session.delete
|
||||||
def new
|
def new
|
||||||
if pre_auth.authorizable?
|
if pre_auth.authorizable?
|
||||||
if skip_authorization? || matching_token?
|
if skip_authorization? || matching_token?
|
||||||
|
@ -16,44 +16,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||||
render "doorkeeper/authorizations/error"
|
render "doorkeeper/authorizations/error"
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_index_vars
|
def set_index_vars
|
||||||
@scopes = Gitlab::Auth::SCOPES
|
@scopes = Gitlab::Auth::API_SCOPES
|
||||||
|
|
||||||
@personal_access_token = finder.build
|
@personal_access_token = finder.build
|
||||||
@inactive_personal_access_tokens = finder(state: 'inactive').execute
|
@inactive_personal_access_tokens = finder(state: 'inactive').execute
|
||||||
|
|
4
app/models/oauth_access_grant.rb
Normal file
4
app/models/oauth_access_grant.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
class OauthAccessGrant < Doorkeeper::AccessGrant
|
||||||
|
belongs_to :resource_owner, class_name: 'User'
|
||||||
|
belongs_to :application, class_name: 'Doorkeeper::Application'
|
||||||
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
class OauthAccessToken < ActiveRecord::Base
|
class OauthAccessToken < Doorkeeper::AccessToken
|
||||||
belongs_to :resource_owner, class_name: 'User'
|
belongs_to :resource_owner, class_name: 'User'
|
||||||
belongs_to :application, class_name: 'Doorkeeper::Application'
|
belongs_to :application, class_name: 'Doorkeeper::Application'
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,9 @@ class PersonalAccessToken < ActiveRecord::Base
|
||||||
scope :with_impersonation, -> { where(impersonation: true) }
|
scope :with_impersonation, -> { where(impersonation: true) }
|
||||||
scope :without_impersonation, -> { where(impersonation: false) }
|
scope :without_impersonation, -> { where(impersonation: false) }
|
||||||
|
|
||||||
|
validates :scopes, presence: true
|
||||||
|
validate :validate_api_scopes
|
||||||
|
|
||||||
def revoke!
|
def revoke!
|
||||||
self.revoked = true
|
self.revoked = true
|
||||||
self.save
|
self.save
|
||||||
|
@ -22,4 +25,12 @@ class PersonalAccessToken < ActiveRecord::Base
|
||||||
def active?
|
def active?
|
||||||
!revoked? && !expired?
|
!revoked? && !expired?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def validate_api_scopes
|
||||||
|
unless scopes.all? { |scope| Gitlab::Auth::API_SCOPES.include?(scope.to_sym) }
|
||||||
|
errors.add :scopes, "can only contain API scopes"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
= hidden_field_tag :state, @pre_auth.state
|
= hidden_field_tag :state, @pre_auth.state
|
||||||
= hidden_field_tag :response_type, @pre_auth.response_type
|
= hidden_field_tag :response_type, @pre_auth.response_type
|
||||||
= hidden_field_tag :scope, @pre_auth.scope
|
= hidden_field_tag :scope, @pre_auth.scope
|
||||||
|
= hidden_field_tag :nonce, @pre_auth.nonce
|
||||||
= submit_tag "Authorize", class: "btn btn-success wide pull-left"
|
= submit_tag "Authorize", class: "btn btn-success wide pull-left"
|
||||||
= form_tag oauth_authorization_path, method: :delete do
|
= form_tag oauth_authorization_path, method: :delete do
|
||||||
= hidden_field_tag :client_id, @pre_auth.client.uid
|
= hidden_field_tag :client_id, @pre_auth.client.uid
|
||||||
|
@ -34,4 +35,5 @@
|
||||||
= hidden_field_tag :state, @pre_auth.state
|
= hidden_field_tag :state, @pre_auth.state
|
||||||
= hidden_field_tag :response_type, @pre_auth.response_type
|
= hidden_field_tag :response_type, @pre_auth.response_type
|
||||||
= hidden_field_tag :scope, @pre_auth.scope
|
= hidden_field_tag :scope, @pre_auth.scope
|
||||||
|
= hidden_field_tag :nonce, @pre_auth.nonce
|
||||||
= submit_tag "Deny", class: "btn btn-danger prepend-left-10"
|
= submit_tag "Deny", class: "btn btn-danger prepend-left-10"
|
||||||
|
|
4
changelogs/unreleased/feature-openid-connect.yml
Normal file
4
changelogs/unreleased/feature-openid-connect.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Implement OpenID Connect identity provider
|
||||||
|
merge_request: 8018
|
||||||
|
author: Markus Koller
|
|
@ -6,9 +6,14 @@ Doorkeeper.configure do
|
||||||
# This block will be called to check whether the resource owner is authenticated or not.
|
# This block will be called to check whether the resource owner is authenticated or not.
|
||||||
resource_owner_authenticator do
|
resource_owner_authenticator do
|
||||||
# Put your resource owner authentication logic here.
|
# Put your resource owner authentication logic here.
|
||||||
|
if current_user
|
||||||
|
current_user
|
||||||
|
else
|
||||||
# Ensure user is redirected to redirect_uri after login
|
# Ensure user is redirected to redirect_uri after login
|
||||||
session[:user_return_to] = request.fullpath
|
session[:user_return_to] = request.fullpath
|
||||||
current_user || redirect_to(new_user_session_url)
|
redirect_to(new_user_session_url)
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resource_owner_from_credentials do |routes|
|
resource_owner_from_credentials do |routes|
|
||||||
|
|
36
config/initializers/doorkeeper_openid_connect.rb
Normal file
36
config/initializers/doorkeeper_openid_connect.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
Doorkeeper::OpenidConnect.configure do
|
||||||
|
issuer Gitlab.config.gitlab.url
|
||||||
|
|
||||||
|
jws_private_key Rails.application.secrets.jws_private_key
|
||||||
|
|
||||||
|
resource_owner_from_access_token do |access_token|
|
||||||
|
User.active.find_by(id: access_token.resource_owner_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
auth_time_from_resource_owner do |user|
|
||||||
|
user.current_sign_in_at
|
||||||
|
end
|
||||||
|
|
||||||
|
reauthenticate_resource_owner do |user, return_to|
|
||||||
|
store_location_for user, return_to
|
||||||
|
sign_out user
|
||||||
|
redirect_to new_user_session_url
|
||||||
|
end
|
||||||
|
|
||||||
|
subject do |user|
|
||||||
|
# hash the user's ID with the Rails secret_key_base to avoid revealing it
|
||||||
|
Digest::SHA256.hexdigest "#{user.id}-#{Rails.application.secrets.secret_key_base}"
|
||||||
|
end
|
||||||
|
|
||||||
|
claims do
|
||||||
|
with_options scope: :openid do |o|
|
||||||
|
o.claim(:name) { |user| user.name }
|
||||||
|
o.claim(:nickname) { |user| user.username }
|
||||||
|
o.claim(:email) { |user| user.public_email }
|
||||||
|
o.claim(:email_verified) { |user| true if user.public_email? }
|
||||||
|
o.claim(:website) { |user| user.full_website_url if user.website_url? }
|
||||||
|
o.claim(:profile) { |user| Rails.application.routes.url_helpers.user_url user }
|
||||||
|
o.claim(:picture) { |user| user.avatar_url }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -24,7 +24,8 @@ def create_tokens
|
||||||
defaults = {
|
defaults = {
|
||||||
secret_key_base: file_secret_key || generate_new_secure_token,
|
secret_key_base: file_secret_key || generate_new_secure_token,
|
||||||
otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token,
|
otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token,
|
||||||
db_key_base: generate_new_secure_token
|
db_key_base: generate_new_secure_token,
|
||||||
|
jws_private_key: generate_new_rsa_private_key
|
||||||
}
|
}
|
||||||
|
|
||||||
missing_secrets = set_missing_keys(defaults)
|
missing_secrets = set_missing_keys(defaults)
|
||||||
|
@ -41,6 +42,10 @@ def generate_new_secure_token
|
||||||
SecureRandom.hex(64)
|
SecureRandom.hex(64)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def generate_new_rsa_private_key
|
||||||
|
OpenSSL::PKey::RSA.new(2048).to_pem
|
||||||
|
end
|
||||||
|
|
||||||
def warn_missing_secret(secret)
|
def warn_missing_secret(secret)
|
||||||
warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml."
|
warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml."
|
||||||
end
|
end
|
||||||
|
|
|
@ -60,6 +60,7 @@ en:
|
||||||
scopes:
|
scopes:
|
||||||
api: Access your API
|
api: Access your API
|
||||||
read_user: Read user information
|
read_user: Read user information
|
||||||
|
openid: Authenticate using OpenID Connect
|
||||||
|
|
||||||
flash:
|
flash:
|
||||||
applications:
|
applications:
|
||||||
|
|
|
@ -22,6 +22,8 @@ Rails.application.routes.draw do
|
||||||
authorizations: 'oauth/authorizations'
|
authorizations: 'oauth/authorizations'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
use_doorkeeper_openid_connect
|
||||||
|
|
||||||
# Autocomplete
|
# Autocomplete
|
||||||
get '/autocomplete/users' => 'autocomplete#users'
|
get '/autocomplete/users' => 'autocomplete#users'
|
||||||
get '/autocomplete/users/:id' => 'autocomplete#user'
|
get '/autocomplete/users/:id' => 'autocomplete#user'
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
class CreateDoorkeeperOpenidConnectTables < ActiveRecord::Migration
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
create_table :oauth_openid_requests do |t|
|
||||||
|
t.integer :access_grant_id, null: false
|
||||||
|
t.string :nonce, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
if Gitlab::Database.postgresql?
|
||||||
|
# add foreign key without validation to avoid downtime on PostgreSQL,
|
||||||
|
# also see db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb
|
||||||
|
execute %q{
|
||||||
|
ALTER TABLE "oauth_openid_requests"
|
||||||
|
ADD CONSTRAINT "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
|
||||||
|
FOREIGN KEY ("access_grant_id")
|
||||||
|
REFERENCES "oauth_access_grants" ("id")
|
||||||
|
NOT VALID;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
execute %q{
|
||||||
|
ALTER TABLE oauth_openid_requests
|
||||||
|
ADD CONSTRAINT fk_oauth_openid_requests_oauth_access_grants_access_grant_id
|
||||||
|
FOREIGN KEY (access_grant_id)
|
||||||
|
REFERENCES oauth_access_grants (id);
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_table :oauth_openid_requests
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,20 @@
|
||||||
|
class ValidateForeignKeysOnOauthOpenidRequests < ActiveRecord::Migration
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
if Gitlab::Database.postgresql?
|
||||||
|
execute %q{
|
||||||
|
ALTER TABLE "oauth_openid_requests"
|
||||||
|
VALIDATE CONSTRAINT "fk_oauth_openid_requests_oauth_access_grants_access_grant_id";
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# noop
|
||||||
|
end
|
||||||
|
end
|
|
@ -878,6 +878,11 @@ ActiveRecord::Schema.define(version: 20170306170512) do
|
||||||
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
|
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
|
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
|
||||||
|
|
||||||
|
create_table "oauth_openid_requests", force: :cascade do |t|
|
||||||
|
t.integer "access_grant_id", null: false
|
||||||
|
t.string "nonce", null: false
|
||||||
|
end
|
||||||
|
|
||||||
create_table "pages_domains", force: :cascade do |t|
|
create_table "pages_domains", force: :cascade do |t|
|
||||||
t.integer "project_id"
|
t.integer "project_id"
|
||||||
t.text "certificate"
|
t.text "certificate"
|
||||||
|
@ -1375,6 +1380,7 @@ ActiveRecord::Schema.define(version: 20170306170512) do
|
||||||
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
|
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
|
||||||
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
|
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
|
||||||
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
|
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
|
||||||
|
add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
|
||||||
add_foreign_key "personal_access_tokens", "users"
|
add_foreign_key "personal_access_tokens", "users"
|
||||||
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
|
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
|
||||||
add_foreign_key "project_authorizations", "users", on_delete: :cascade
|
add_foreign_key "project_authorizations", "users", on_delete: :cascade
|
||||||
|
|
|
@ -12,6 +12,7 @@ See the documentation below for details on how to configure these services.
|
||||||
- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
|
- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
|
||||||
- [CAS](cas.md) Configure GitLab to sign in using CAS
|
- [CAS](cas.md) Configure GitLab to sign in using CAS
|
||||||
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
|
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
|
||||||
|
- [OpenID Connect](openid_connect_provider.md) Use GitLab as an identity provider
|
||||||
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
|
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
|
||||||
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
|
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
|
||||||
- [Akismet](akismet.md) Configure Akismet to stop spam
|
- [Akismet](akismet.md) Configure Akismet to stop spam
|
||||||
|
|
47
doc/integration/openid_connect_provider.md
Normal file
47
doc/integration/openid_connect_provider.md
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# GitLab as OpenID Connect identity provider
|
||||||
|
|
||||||
|
This document is about using GitLab as an OpenID Connect identity provider
|
||||||
|
to sign in to other services.
|
||||||
|
|
||||||
|
## Introduction to OpenID Connect
|
||||||
|
|
||||||
|
[OpenID Connect] \(OIC) is a simple identity layer on top of the
|
||||||
|
OAuth 2.0 protocol. It allows clients to verify the identity of the end-user
|
||||||
|
based on the authentication performed by GitLab, as well as to obtain
|
||||||
|
basic profile information about the end-user in an interoperable and
|
||||||
|
REST-like manner. OIC performs many of the same tasks as OpenID 2.0,
|
||||||
|
but does so in a way that is API-friendly, and usable by native and
|
||||||
|
mobile applications.
|
||||||
|
|
||||||
|
On the client side, you can use [omniauth-openid-connect] for Rails
|
||||||
|
applications, or any of the other available [client implementations].
|
||||||
|
|
||||||
|
GitLab's implementation uses the [doorkeeper-openid_connect] gem, refer
|
||||||
|
to its README for more details about which parts of the specifications
|
||||||
|
are supported.
|
||||||
|
|
||||||
|
## Enabling OpenID Connect for OAuth applications
|
||||||
|
|
||||||
|
Refer to the [OAuth guide] for basic information on how to set up OAuth
|
||||||
|
applications in GitLab. To enable OIC for an application, all you have to do
|
||||||
|
is select the `openid` scope in the application settings.
|
||||||
|
|
||||||
|
Currently the following user information is shared with clients:
|
||||||
|
|
||||||
|
| Claim | Type | Description |
|
||||||
|
|:-----------------|:----------|:------------|
|
||||||
|
| `sub` | `string` | An opaque token that uniquely identifies the user
|
||||||
|
| `auth_time` | `integer` | The timestamp for the user's last authentication
|
||||||
|
| `name` | `string` | The user's full name
|
||||||
|
| `nickname` | `string` | The user's GitLab username
|
||||||
|
| `email` | `string` | The user's public email address
|
||||||
|
| `email_verified` | `boolean` | Whether the user's public email address was verified
|
||||||
|
| `website` | `string` | URL for the user's website
|
||||||
|
| `profile` | `string` | URL for the user's GitLab profile
|
||||||
|
| `picture` | `string` | URL for the user's GitLab avatar
|
||||||
|
|
||||||
|
[OpenID Connect]: http://openid.net/connect/ "OpenID Connect website"
|
||||||
|
[doorkeeper-openid_connect]: https://github.com/doorkeeper-gem/doorkeeper-openid_connect "Doorkeeper::OpenidConnect website"
|
||||||
|
[OAuth guide]: oauth_provider.md "GitLab as OAuth2 authentication service provider"
|
||||||
|
[omniauth-openid-connect]: https://github.com/jjbohn/omniauth-openid-connect/ "OmniAuth::OpenIDConnect website"
|
||||||
|
[client implementations]: http://openid.net/developers/libraries#connect "List of available client implementations"
|
|
@ -2,9 +2,17 @@ module Gitlab
|
||||||
module Auth
|
module Auth
|
||||||
MissingPersonalTokenError = Class.new(StandardError)
|
MissingPersonalTokenError = Class.new(StandardError)
|
||||||
|
|
||||||
SCOPES = [:api, :read_user].freeze
|
# Scopes used for GitLab API access
|
||||||
|
API_SCOPES = [:api, :read_user].freeze
|
||||||
|
|
||||||
|
# Scopes used for OpenID Connect
|
||||||
|
OPENID_SCOPES = [:openid].freeze
|
||||||
|
|
||||||
|
# Default scopes for OAuth applications that don't define their own
|
||||||
DEFAULT_SCOPES = [:api].freeze
|
DEFAULT_SCOPES = [:api].freeze
|
||||||
OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES
|
|
||||||
|
# Other available scopes
|
||||||
|
OPTIONAL_SCOPES = (API_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def find_for_git_client(login, password, project:, ip:)
|
def find_for_git_client(login, password, project:, ip:)
|
||||||
|
@ -40,7 +48,7 @@ module Gitlab
|
||||||
|
|
||||||
Gitlab::LDAP::Authentication.login(login, password)
|
Gitlab::LDAP::Authentication.login(login, password)
|
||||||
else
|
else
|
||||||
user if user.valid_password?(password)
|
user if user.active? && user.valid_password?(password)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
65
spec/controllers/admin/applications_controller_spec.rb
Normal file
65
spec/controllers/admin/applications_controller_spec.rb
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Admin::ApplicationsController do
|
||||||
|
let(:admin) { create(:admin) }
|
||||||
|
let(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #new' do
|
||||||
|
it 'renders the application form' do
|
||||||
|
get :new
|
||||||
|
|
||||||
|
expect(response).to render_template :new
|
||||||
|
expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #edit' do
|
||||||
|
it 'renders the application form' do
|
||||||
|
get :edit, id: application.id
|
||||||
|
|
||||||
|
expect(response).to render_template :edit
|
||||||
|
expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #create' do
|
||||||
|
it 'creates the application' do
|
||||||
|
expect do
|
||||||
|
post :create, doorkeeper_application: attributes_for(:application)
|
||||||
|
end.to change { Doorkeeper::Application.count }.by(1)
|
||||||
|
|
||||||
|
application = Doorkeeper::Application.last
|
||||||
|
|
||||||
|
expect(response).to redirect_to(admin_application_path(application))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the application form on errors' do
|
||||||
|
expect do
|
||||||
|
post :create, doorkeeper_application: attributes_for(:application).merge(redirect_uri: nil)
|
||||||
|
end.not_to change { Doorkeeper::Application.count }
|
||||||
|
|
||||||
|
expect(response).to render_template :new
|
||||||
|
expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH #update' do
|
||||||
|
it 'updates the application' do
|
||||||
|
patch :update, id: application.id, doorkeeper_application: { redirect_uri: 'http://example.com/' }
|
||||||
|
|
||||||
|
expect(response).to redirect_to(admin_application_path(application))
|
||||||
|
expect(application.reload.redirect_uri).to eq 'http://example.com/'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the application form on errors' do
|
||||||
|
patch :update, id: application.id, doorkeeper_application: { redirect_uri: nil }
|
||||||
|
|
||||||
|
expect(response).to render_template :edit
|
||||||
|
expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,6 +2,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
describe Profiles::PersonalAccessTokensController do
|
describe Profiles::PersonalAccessTokensController do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
let(:token_attributes) { attributes_for(:personal_access_token) }
|
||||||
|
|
||||||
before { sign_in(user) }
|
before { sign_in(user) }
|
||||||
|
|
||||||
|
@ -10,41 +11,26 @@ describe Profiles::PersonalAccessTokensController do
|
||||||
PersonalAccessToken.order(:created_at).last
|
PersonalAccessToken.order(:created_at).last
|
||||||
end
|
end
|
||||||
|
|
||||||
it "allows creation of a token" do
|
it "allows creation of a token with scopes" do
|
||||||
name = FFaker::Product.brand
|
name = FFaker::Product.brand
|
||||||
|
scopes = %w[api read_user]
|
||||||
|
|
||||||
post :create, personal_access_token: { name: name }
|
post :create, personal_access_token: token_attributes.merge(scopes: scopes)
|
||||||
|
|
||||||
expect(created_token).not_to be_nil
|
expect(created_token).not_to be_nil
|
||||||
expect(created_token.name).to eq(name)
|
expect(created_token.name).to eq(token_attributes[:name])
|
||||||
expect(created_token.expires_at).to be_nil
|
expect(created_token.scopes).to eq(scopes)
|
||||||
expect(PersonalAccessToken.active).to include(created_token)
|
expect(PersonalAccessToken.active).to include(created_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "allows creation of a token with an expiry date" do
|
it "allows creation of a token with an expiry date" do
|
||||||
expires_at = 5.days.from_now.to_date
|
expires_at = 5.days.from_now.to_date
|
||||||
|
|
||||||
post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at }
|
post :create, personal_access_token: token_attributes.merge(expires_at: expires_at)
|
||||||
|
|
||||||
expect(created_token).not_to be_nil
|
expect(created_token).not_to be_nil
|
||||||
expect(created_token.expires_at).to eq(expires_at)
|
expect(created_token.expires_at).to eq(expires_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "scopes" do
|
|
||||||
it "allows creation of a token with scopes" do
|
|
||||||
post :create, personal_access_token: { name: FFaker::Product.brand, scopes: %w(api read_user) }
|
|
||||||
|
|
||||||
expect(created_token).not_to be_nil
|
|
||||||
expect(created_token.scopes).to eq(%w(api read_user))
|
|
||||||
end
|
|
||||||
|
|
||||||
it "allows creation of a token with no scopes" do
|
|
||||||
post :create, personal_access_token: { name: FFaker::Product.brand, scopes: [] }
|
|
||||||
|
|
||||||
expect(created_token).not_to be_nil
|
|
||||||
expect(created_token.scopes).to eq([])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#index' do
|
describe '#index' do
|
||||||
|
|
11
spec/factories/oauth_access_grants.rb
Normal file
11
spec/factories/oauth_access_grants.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
FactoryGirl.define do
|
||||||
|
factory :oauth_access_grant do
|
||||||
|
resource_owner_id { create(:user).id }
|
||||||
|
application
|
||||||
|
token { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
|
||||||
|
expires_in 2.hours
|
||||||
|
|
||||||
|
redirect_uri { application.redirect_uri }
|
||||||
|
scopes { application.scopes }
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,6 +2,7 @@ FactoryGirl.define do
|
||||||
factory :oauth_access_token do
|
factory :oauth_access_token do
|
||||||
resource_owner
|
resource_owner
|
||||||
application
|
application
|
||||||
token '123456'
|
token { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
|
||||||
|
scopes { application.scopes }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
FactoryGirl.define do
|
FactoryGirl.define do
|
||||||
factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do
|
factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do
|
||||||
name { FFaker::Name.name }
|
name { FFaker::Name.name }
|
||||||
uid { FFaker::Name.name }
|
uid { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
|
||||||
redirect_uri { FFaker::Internet.uri('http') }
|
redirect_uri { FFaker::Internet.uri('http') }
|
||||||
owner
|
owner
|
||||||
owner_type 'User'
|
owner_type 'User'
|
||||||
|
|
71
spec/initializers/doorkeeper_spec.rb
Normal file
71
spec/initializers/doorkeeper_spec.rb
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require_relative '../../config/initializers/doorkeeper'
|
||||||
|
|
||||||
|
describe Doorkeeper.configuration do
|
||||||
|
describe '#default_scopes' do
|
||||||
|
it 'matches Gitlab::Auth::DEFAULT_SCOPES' do
|
||||||
|
expect(subject.default_scopes).to eq Gitlab::Auth::DEFAULT_SCOPES
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#optional_scopes' do
|
||||||
|
it 'matches Gitlab::Auth::OPTIONAL_SCOPES' do
|
||||||
|
expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#resource_owner_authenticator' do
|
||||||
|
subject { controller.instance_exec(&Doorkeeper.configuration.authenticate_resource_owner) }
|
||||||
|
|
||||||
|
let(:controller) { double }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:current_user).and_return(current_user)
|
||||||
|
allow(controller).to receive(:session).and_return({})
|
||||||
|
allow(controller).to receive(:request).and_return(OpenStruct.new(fullpath: '/return-path'))
|
||||||
|
allow(controller).to receive(:redirect_to)
|
||||||
|
allow(controller).to receive(:new_user_session_url).and_return('/login')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a user present' do
|
||||||
|
let(:current_user) { create(:user) }
|
||||||
|
|
||||||
|
it 'returns the user' do
|
||||||
|
expect(subject).to eq current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not redirect' do
|
||||||
|
expect(controller).not_to receive(:redirect_to)
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not store the return path' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(controller.session).not_to include :user_return_to
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without a user present' do
|
||||||
|
let(:current_user) { nil }
|
||||||
|
|
||||||
|
# NOTE: this is required for doorkeeper-openid_connect
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(subject).to eq nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to the login form' do
|
||||||
|
expect(controller).to receive(:redirect_to).with('/login')
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores the return path' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(controller.session[:user_return_to]).to eq '/return-path'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,6 +6,9 @@ describe 'create_tokens', lib: true do
|
||||||
|
|
||||||
let(:secrets) { ActiveSupport::OrderedOptions.new }
|
let(:secrets) { ActiveSupport::OrderedOptions.new }
|
||||||
|
|
||||||
|
HEX_KEY = /\h{128}/
|
||||||
|
RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(File).to receive(:write)
|
allow(File).to receive(:write)
|
||||||
allow(File).to receive(:delete)
|
allow(File).to receive(:delete)
|
||||||
|
@ -15,7 +18,7 @@ describe 'create_tokens', lib: true do
|
||||||
allow(self).to receive(:exit)
|
allow(self).to receive(:exit)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'setting secret_key_base and otp_key_base' do
|
context 'setting secret keys' do
|
||||||
context 'when none of the secrets exist' do
|
context 'when none of the secrets exist' do
|
||||||
before do
|
before do
|
||||||
stub_env('SECRET_KEY_BASE', nil)
|
stub_env('SECRET_KEY_BASE', nil)
|
||||||
|
@ -24,19 +27,29 @@ describe 'create_tokens', lib: true do
|
||||||
allow(self).to receive(:warn_missing_secret)
|
allow(self).to receive(:warn_missing_secret)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'generates different secrets for secret_key_base, otp_key_base, and db_key_base' do
|
it 'generates different hashes for secret_key_base, otp_key_base, and db_key_base' do
|
||||||
create_tokens
|
create_tokens
|
||||||
|
|
||||||
keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base)
|
keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base)
|
||||||
|
|
||||||
expect(keys.uniq).to eq(keys)
|
expect(keys.uniq).to eq(keys)
|
||||||
expect(keys.map(&:length)).to all(eq(128))
|
expect(keys).to all(match(HEX_KEY))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'generates an RSA key for jws_private_key' do
|
||||||
|
create_tokens
|
||||||
|
|
||||||
|
keys = secrets.values_at(:jws_private_key)
|
||||||
|
|
||||||
|
expect(keys.uniq).to eq(keys)
|
||||||
|
expect(keys).to all(match(RSA_KEY))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'warns about the secrets to add to secrets.yml' do
|
it 'warns about the secrets to add to secrets.yml' do
|
||||||
expect(self).to receive(:warn_missing_secret).with('secret_key_base')
|
expect(self).to receive(:warn_missing_secret).with('secret_key_base')
|
||||||
expect(self).to receive(:warn_missing_secret).with('otp_key_base')
|
expect(self).to receive(:warn_missing_secret).with('otp_key_base')
|
||||||
expect(self).to receive(:warn_missing_secret).with('db_key_base')
|
expect(self).to receive(:warn_missing_secret).with('db_key_base')
|
||||||
|
expect(self).to receive(:warn_missing_secret).with('jws_private_key')
|
||||||
|
|
||||||
create_tokens
|
create_tokens
|
||||||
end
|
end
|
||||||
|
@ -48,6 +61,7 @@ describe 'create_tokens', lib: true do
|
||||||
expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base)
|
expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base)
|
||||||
expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base)
|
expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base)
|
||||||
expect(new_secrets['db_key_base']).to eq(secrets.db_key_base)
|
expect(new_secrets['db_key_base']).to eq(secrets.db_key_base)
|
||||||
|
expect(new_secrets['jws_private_key']).to eq(secrets.jws_private_key)
|
||||||
end
|
end
|
||||||
|
|
||||||
create_tokens
|
create_tokens
|
||||||
|
@ -63,6 +77,7 @@ describe 'create_tokens', lib: true do
|
||||||
context 'when the other secrets all exist' do
|
context 'when the other secrets all exist' do
|
||||||
before do
|
before do
|
||||||
secrets.db_key_base = 'db_key_base'
|
secrets.db_key_base = 'db_key_base'
|
||||||
|
secrets.jws_private_key = 'jws_private_key'
|
||||||
|
|
||||||
allow(File).to receive(:exist?).with('.secret').and_return(true)
|
allow(File).to receive(:exist?).with('.secret').and_return(true)
|
||||||
allow(File).to receive(:read).with('.secret').and_return('file_key')
|
allow(File).to receive(:read).with('.secret').and_return('file_key')
|
||||||
|
@ -73,6 +88,7 @@ describe 'create_tokens', lib: true do
|
||||||
stub_env('SECRET_KEY_BASE', 'env_key')
|
stub_env('SECRET_KEY_BASE', 'env_key')
|
||||||
secrets.secret_key_base = 'secret_key_base'
|
secrets.secret_key_base = 'secret_key_base'
|
||||||
secrets.otp_key_base = 'otp_key_base'
|
secrets.otp_key_base = 'otp_key_base'
|
||||||
|
secrets.jws_private_key = 'jws_private_key'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not issue a warning' do
|
it 'does not issue a warning' do
|
||||||
|
@ -98,6 +114,7 @@ describe 'create_tokens', lib: true do
|
||||||
before do
|
before do
|
||||||
secrets.secret_key_base = 'secret_key_base'
|
secrets.secret_key_base = 'secret_key_base'
|
||||||
secrets.otp_key_base = 'otp_key_base'
|
secrets.otp_key_base = 'otp_key_base'
|
||||||
|
secrets.jws_private_key = 'jws_private_key'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not write any files' do
|
it 'does not write any files' do
|
||||||
|
@ -112,6 +129,7 @@ describe 'create_tokens', lib: true do
|
||||||
expect(secrets.secret_key_base).to eq('secret_key_base')
|
expect(secrets.secret_key_base).to eq('secret_key_base')
|
||||||
expect(secrets.otp_key_base).to eq('otp_key_base')
|
expect(secrets.otp_key_base).to eq('otp_key_base')
|
||||||
expect(secrets.db_key_base).to eq('db_key_base')
|
expect(secrets.db_key_base).to eq('db_key_base')
|
||||||
|
expect(secrets.jws_private_key).to eq('jws_private_key')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'deletes the .secret file' do
|
it 'deletes the .secret file' do
|
||||||
|
@ -135,6 +153,7 @@ describe 'create_tokens', lib: true do
|
||||||
expect(new_secrets['secret_key_base']).to eq('file_key')
|
expect(new_secrets['secret_key_base']).to eq('file_key')
|
||||||
expect(new_secrets['otp_key_base']).to eq('file_key')
|
expect(new_secrets['otp_key_base']).to eq('file_key')
|
||||||
expect(new_secrets['db_key_base']).to eq('db_key_base')
|
expect(new_secrets['db_key_base']).to eq('db_key_base')
|
||||||
|
expect(new_secrets['jws_private_key']).to eq('jws_private_key')
|
||||||
end
|
end
|
||||||
|
|
||||||
create_tokens
|
create_tokens
|
||||||
|
|
|
@ -3,6 +3,24 @@ require 'spec_helper'
|
||||||
describe Gitlab::Auth, lib: true do
|
describe Gitlab::Auth, lib: true do
|
||||||
let(:gl_auth) { described_class }
|
let(:gl_auth) { described_class }
|
||||||
|
|
||||||
|
describe 'constants' do
|
||||||
|
it 'API_SCOPES contains all scopes for API access' do
|
||||||
|
expect(subject::API_SCOPES).to eq [:api, :read_user]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
|
||||||
|
expect(subject::OPENID_SCOPES).to eq [:openid]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'DEFAULT_SCOPES contains all default scopes' do
|
||||||
|
expect(subject::DEFAULT_SCOPES).to eq [:api]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'OPTIONAL_SCOPES contains all non-default scopes' do
|
||||||
|
expect(subject::OPTIONAL_SCOPES).to eq [:read_user, :openid]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'find_for_git_client' do
|
describe 'find_for_git_client' do
|
||||||
context 'build token' do
|
context 'build token' do
|
||||||
subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: project, ip: 'ip') }
|
subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: project, ip: 'ip') }
|
||||||
|
@ -222,6 +240,18 @@ describe Gitlab::Auth, lib: true do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "does not find user in blocked state" do
|
||||||
|
user.block
|
||||||
|
|
||||||
|
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not find user in ldap_blocked state" do
|
||||||
|
user.ldap_block
|
||||||
|
|
||||||
|
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
|
||||||
|
end
|
||||||
|
|
||||||
context "with ldap enabled" do
|
context "with ldap enabled" do
|
||||||
before do
|
before do
|
||||||
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
|
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
|
||||||
|
|
|
@ -34,4 +34,28 @@ describe PersonalAccessToken, models: true do
|
||||||
expect(active_personal_access_token).to be_active
|
expect(active_personal_access_token).to be_active
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "validations" do
|
||||||
|
let(:personal_access_token) { build(:personal_access_token) }
|
||||||
|
|
||||||
|
it "requires at least one scope" do
|
||||||
|
personal_access_token.scopes = []
|
||||||
|
|
||||||
|
expect(personal_access_token).not_to be_valid
|
||||||
|
expect(personal_access_token.errors[:scopes].first).to eq "can't be blank"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows creating a token with API scopes" do
|
||||||
|
personal_access_token.scopes = [:api, :read_user]
|
||||||
|
|
||||||
|
expect(personal_access_token).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects creating a token with non-API scopes" do
|
||||||
|
personal_access_token.scopes = [:openid, :api]
|
||||||
|
|
||||||
|
expect(personal_access_token).not_to be_valid
|
||||||
|
expect(personal_access_token.errors[:scopes].first).to eq "can only contain API scopes"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,4 +39,22 @@ describe API::API, api: true do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "when user is blocked" do
|
||||||
|
it "returns authentication error" do
|
||||||
|
user.block
|
||||||
|
get api("/user"), access_token: token.token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when user is ldap_blocked" do
|
||||||
|
it "returns authentication error" do
|
||||||
|
user.ldap_block
|
||||||
|
get api("/user"), access_token: token.token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,5 +29,27 @@ describe API::API, api: true do
|
||||||
expect(json_response['access_token']).not_to be_nil
|
expect(json_response['access_token']).not_to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when user is blocked" do
|
||||||
|
it "does not create an access token" do
|
||||||
|
user = create(:user)
|
||||||
|
user.block
|
||||||
|
|
||||||
|
request_oauth_token(user)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when user is ldap_blocked" do
|
||||||
|
it "does not create an access token" do
|
||||||
|
user = create(:user)
|
||||||
|
user.ldap_block
|
||||||
|
|
||||||
|
request_oauth_token(user)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -87,5 +87,23 @@ describe API::Session, api: true do
|
||||||
expect(response).to have_http_status(400)
|
expect(response).to have_http_status(400)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when user is blocked" do
|
||||||
|
it "returns authentication error" do
|
||||||
|
user.block
|
||||||
|
post api("/session"), email: user.username, password: user.password
|
||||||
|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when user is ldap_blocked" do
|
||||||
|
it "returns authentication error" do
|
||||||
|
user.ldap_block
|
||||||
|
post api("/session"), email: user.username, password: user.password
|
||||||
|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -221,12 +221,20 @@ describe 'Git HTTP requests', lib: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the user is blocked" do
|
context "when the user is blocked" do
|
||||||
it "responds with status 404" do
|
it "responds with status 401" do
|
||||||
user.block
|
user.block
|
||||||
project.team << [user, :master]
|
project.team << [user, :master]
|
||||||
|
|
||||||
download(path, env) do |response|
|
download(path, env) do |response|
|
||||||
expect(response).to have_http_status(404)
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "responds with status 401 for unknown projects (no project existence information leak)" do
|
||||||
|
user.block
|
||||||
|
|
||||||
|
download('doesnt/exist.git', env) do |response|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
134
spec/requests/openid_connect_spec.rb
Normal file
134
spec/requests/openid_connect_spec.rb
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe 'OpenID Connect requests' do
|
||||||
|
include ApiHelpers
|
||||||
|
|
||||||
|
let(:user) { create :user }
|
||||||
|
let(:access_grant) { create :oauth_access_grant, application: application, resource_owner_id: user.id }
|
||||||
|
let(:access_token) { create :oauth_access_token, application: application, resource_owner_id: user.id }
|
||||||
|
|
||||||
|
def request_access_token
|
||||||
|
login_as user
|
||||||
|
|
||||||
|
post '/oauth/token',
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: access_grant.token,
|
||||||
|
redirect_uri: application.redirect_uri,
|
||||||
|
client_id: application.uid,
|
||||||
|
client_secret: application.secret
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_user_info
|
||||||
|
get '/oauth/userinfo', nil, 'Authorization' => "Bearer #{access_token.token}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def hashed_subject
|
||||||
|
Digest::SHA256.hexdigest("#{user.id}-#{Rails.application.secrets.secret_key_base}")
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'Application without OpenID scope' do
|
||||||
|
let(:application) { create :oauth_application, scopes: 'api' }
|
||||||
|
|
||||||
|
it 'token response does not include an ID token' do
|
||||||
|
request_access_token
|
||||||
|
|
||||||
|
expect(json_response).to include 'access_token'
|
||||||
|
expect(json_response).not_to include 'id_token'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'userinfo response is unauthorized' do
|
||||||
|
request_user_info
|
||||||
|
|
||||||
|
expect(response).to have_http_status 403
|
||||||
|
expect(response.body).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'Application with OpenID scope' do
|
||||||
|
let(:application) { create :oauth_application, scopes: 'openid' }
|
||||||
|
|
||||||
|
it 'token response includes an ID token' do
|
||||||
|
request_access_token
|
||||||
|
|
||||||
|
expect(json_response).to include 'id_token'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'UserInfo payload' do
|
||||||
|
let(:user) do
|
||||||
|
create(
|
||||||
|
:user,
|
||||||
|
name: 'Alice',
|
||||||
|
username: 'alice',
|
||||||
|
emails: [private_email, public_email],
|
||||||
|
email: private_email.email,
|
||||||
|
public_email: public_email.email,
|
||||||
|
website_url: 'https://example.com',
|
||||||
|
avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png"),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:public_email) { build :email, email: 'public@example.com' }
|
||||||
|
let(:private_email) { build :email, email: 'private@example.com' }
|
||||||
|
|
||||||
|
it 'includes all user information' do
|
||||||
|
request_user_info
|
||||||
|
|
||||||
|
expect(json_response).to eq({
|
||||||
|
'sub' => hashed_subject,
|
||||||
|
'name' => 'Alice',
|
||||||
|
'nickname' => 'alice',
|
||||||
|
'email' => 'public@example.com',
|
||||||
|
'email_verified' => true,
|
||||||
|
'website' => 'https://example.com',
|
||||||
|
'profile' => 'http://localhost/alice',
|
||||||
|
'picture' => "http://localhost/uploads/user/avatar/#{user.id}/dk.png",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'ID token payload' do
|
||||||
|
before do
|
||||||
|
request_access_token
|
||||||
|
@payload = JSON::JWT.decode(json_response['id_token'], :skip_verification)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes the Gitlab root URL' do
|
||||||
|
expect(@payload['iss']).to eq Gitlab.config.gitlab.url
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes the hashed user ID' do
|
||||||
|
expect(@payload['sub']).to eq hashed_subject
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes the time of the last authentication' do
|
||||||
|
expect(@payload['auth_time']).to eq user.current_sign_in_at.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include any unknown properties' do
|
||||||
|
expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is blocked' do
|
||||||
|
it 'returns authentication error' do
|
||||||
|
access_grant
|
||||||
|
user.block
|
||||||
|
|
||||||
|
expect do
|
||||||
|
request_access_token
|
||||||
|
end.to throw_symbol :warden
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is ldap_blocked' do
|
||||||
|
it 'returns authentication error' do
|
||||||
|
access_grant
|
||||||
|
user.ldap_block
|
||||||
|
|
||||||
|
expect do
|
||||||
|
request_access_token
|
||||||
|
end.to throw_symbol :warden
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
30
spec/routing/openid_connect_spec.rb
Normal file
30
spec/routing/openid_connect_spec.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
# oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys
|
||||||
|
# oauth_discovery_provider GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider
|
||||||
|
# oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger
|
||||||
|
describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
|
||||||
|
it "to #provider" do
|
||||||
|
expect(get('/.well-known/openid-configuration')).to route_to('doorkeeper/openid_connect/discovery#provider')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "to #webfinger" do
|
||||||
|
expect(get('/.well-known/webfinger')).to route_to('doorkeeper/openid_connect/discovery#webfinger')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "to #keys" do
|
||||||
|
expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show
|
||||||
|
# POST /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show
|
||||||
|
describe Doorkeeper::OpenidConnect::UserinfoController, 'routing' do
|
||||||
|
it "to #show" do
|
||||||
|
expect(get('/oauth/userinfo')).to route_to('doorkeeper/openid_connect/userinfo#show')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "to #show" do
|
||||||
|
expect(post('/oauth/userinfo')).to route_to('doorkeeper/openid_connect/userinfo#show')
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue