Merge branch 'siemens/gitlab-ce-feature/openid-connect'

This commit is contained in:
Sean McGivern 2017-03-07 16:16:08 +00:00
commit de37dcee90
36 changed files with 731 additions and 131 deletions

103
Gemfile
View File

@ -18,25 +18,26 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.24.0'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.2'
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'doorkeeper-openid_connect', '~> 1.1.0'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.4.1'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.0'
gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6'
gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.0'
gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6'
# Spam and anti-bot protection
gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
@ -68,9 +69,9 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API
gem 'grape', '~> 0.19.0'
gem 'grape', '~> 0.19.0'
gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
# Pagination
gem 'kaminari', '~> 0.17.0'
@ -102,19 +103,19 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.4'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.4'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
@ -229,18 +230,18 @@ gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.0'
gem 'gon', '~> 6.1.0'
gem 'gemojione', '~> 3.0'
gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0'
gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
gem 'jquery-rails', '~> 4.1.0'
gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
# Sentry integration
gem 'sentry-raven', '~> 2.0.0'
@ -278,13 +279,13 @@ group :development, :test do
gem 'awesome_print', '~> 1.2.0', require: false
gem 'fuubar', '~> 2.0.0'
gem 'database_cleaner', '~> 1.5.0'
gem 'database_cleaner', '~> 1.5.0'
gem 'factory_girl_rails', '~> 4.7.0'
gem 'rspec-rails', '~> 3.5.0'
gem 'rspec-retry', '~> 0.4.5'
gem 'spinach-rails', '~> 0.2.1'
gem 'rspec-rails', '~> 3.5.0'
gem 'rspec-retry', '~> 0.4.5'
gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec_profiling', '~> 0.0.5'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
@ -292,13 +293,13 @@ group :development, :test do
# Generate Fake data
gem 'ffaker', '~> 2.4'
gem 'capybara', '~> 2.6.2'
gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0'
gem 'poltergeist', '~> 1.9.0'
gem 'spring', '~> 1.7.0'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring', '~> 1.7.0'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'rubocop', '~> 0.47.1', require: false
gem 'rubocop-rspec', '~> 1.12.0', require: false

View File

@ -78,6 +78,7 @@ GEM
better_errors (1.0.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6)
@ -167,6 +168,9 @@ GEM
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0)
railties (>= 4.2)
doorkeeper-openid_connect (1.1.2)
doorkeeper (~> 4.0)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
rails (> 3.1)
email_reply_trimmer (0.1.6)
@ -376,6 +380,12 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.6)
json-jwt (1.7.1)
activesupport
bindata
multi_json (>= 1.3)
securecompare
url_safe_base64
json-schema (2.6.2)
addressable (~> 2.3.8)
jwt (1.5.6)
@ -684,6 +694,7 @@ GEM
scss_lint (0.47.1)
rake (>= 0.9, < 11)
sass (~> 3.4.15)
securecompare (1.0.0)
seed-fu (2.3.6)
activerecord (>= 3.1)
activesupport (>= 3.1)
@ -789,6 +800,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
url_safe_base64 (0.2.2)
validates_hostname (1.0.6)
activerecord (>= 3.0)
activesupport (>= 3.0)
@ -866,6 +878,7 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
doorkeeper-openid_connect (~> 1.1.0)
dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)

View File

@ -2,7 +2,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
include OauthApplications
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
@applications = Doorkeeper::Application.where("owner_id IS NULL")

View File

@ -1,8 +1,8 @@
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
before_action :authenticate_resource_owner!
layout 'profile'
# Overriden from Doorkeeper::AuthorizationsController to
# include the call to session.delete
def new
if pre_auth.authorizable?
if skip_authorization? || matching_token?
@ -16,44 +16,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
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

View File

@ -38,7 +38,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def set_index_vars
@scopes = Gitlab::Auth::SCOPES
@scopes = Gitlab::Auth::API_SCOPES
@personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute

View File

@ -0,0 +1,4 @@
class OauthAccessGrant < Doorkeeper::AccessGrant
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
end

View File

@ -1,4 +1,4 @@
class OauthAccessToken < ActiveRecord::Base
class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
end

View File

@ -14,6 +14,9 @@ class PersonalAccessToken < ActiveRecord::Base
scope :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) }
validates :scopes, presence: true
validate :validate_api_scopes
def revoke!
self.revoked = true
self.save
@ -22,4 +25,12 @@ class PersonalAccessToken < ActiveRecord::Base
def active?
!revoked? && !expired?
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

View File

@ -27,6 +27,7 @@
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
= hidden_field_tag :nonce, @pre_auth.nonce
= 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
@ -34,4 +35,5 @@
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
= hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Deny", class: "btn btn-danger prepend-left-10"

View File

@ -0,0 +1,4 @@
---
title: Implement OpenID Connect identity provider
merge_request: 8018
author: Markus Koller

View File

@ -6,9 +6,14 @@ Doorkeeper.configure do
# 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.
# Ensure user is redirected to redirect_uri after login
session[:user_return_to] = request.fullpath
current_user || redirect_to(new_user_session_url)
if current_user
current_user
else
# Ensure user is redirected to redirect_uri after login
session[:user_return_to] = request.fullpath
redirect_to(new_user_session_url)
nil
end
end
resource_owner_from_credentials do |routes|

View 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

View File

@ -24,7 +24,8 @@ def create_tokens
defaults = {
secret_key_base: 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)
@ -41,6 +42,10 @@ def generate_new_secure_token
SecureRandom.hex(64)
end
def generate_new_rsa_private_key
OpenSSL::PKey::RSA.new(2048).to_pem
end
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."
end

View File

@ -60,6 +60,7 @@ en:
scopes:
api: Access your API
read_user: Read user information
openid: Authenticate using OpenID Connect
flash:
applications:

View File

@ -22,6 +22,8 @@ Rails.application.routes.draw do
authorizations: 'oauth/authorizations'
end
use_doorkeeper_openid_connect
# Autocomplete
get '/autocomplete/users' => 'autocomplete#users'
get '/autocomplete/users/:id' => 'autocomplete#user'

View File

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

View File

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

View File

@ -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", ["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|
t.integer "project_id"
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_requests_closing_issues", "issues", 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 "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade

View File

@ -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
- [CAS](cas.md) Configure GitLab to sign in using CAS
- [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
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
- [Akismet](akismet.md) Configure Akismet to stop spam

View 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"

View File

@ -2,9 +2,17 @@ module Gitlab
module Auth
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
OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES
# Other available scopes
OPTIONAL_SCOPES = (API_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze
class << self
def find_for_git_client(login, password, project:, ip:)
@ -40,7 +48,7 @@ module Gitlab
Gitlab::LDAP::Authentication.login(login, password)
else
user if user.valid_password?(password)
user if user.active? && user.valid_password?(password)
end
end
end

View 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

View File

@ -2,6 +2,7 @@ require 'spec_helper'
describe Profiles::PersonalAccessTokensController do
let(:user) { create(:user) }
let(:token_attributes) { attributes_for(:personal_access_token) }
before { sign_in(user) }
@ -10,41 +11,26 @@ describe Profiles::PersonalAccessTokensController do
PersonalAccessToken.order(:created_at).last
end
it "allows creation of a token" do
it "allows creation of a token with scopes" do
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.name).to eq(name)
expect(created_token.expires_at).to be_nil
expect(created_token.name).to eq(token_attributes[:name])
expect(created_token.scopes).to eq(scopes)
expect(PersonalAccessToken.active).to include(created_token)
end
it "allows creation of a token with an expiry date" do
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.expires_at).to eq(expires_at)
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
describe '#index' do

View 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

View File

@ -2,6 +2,7 @@ FactoryGirl.define do
factory :oauth_access_token do
resource_owner
application
token '123456'
token { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
scopes { application.scopes }
end
end

View File

@ -1,7 +1,7 @@
FactoryGirl.define do
factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do
name { FFaker::Name.name }
uid { FFaker::Name.name }
uid { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
redirect_uri { FFaker::Internet.uri('http') }
owner
owner_type 'User'

View 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

View File

@ -6,6 +6,9 @@ describe 'create_tokens', lib: true do
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
allow(File).to receive(:write)
allow(File).to receive(:delete)
@ -15,7 +18,7 @@ describe 'create_tokens', lib: true do
allow(self).to receive(:exit)
end
context 'setting secret_key_base and otp_key_base' do
context 'setting secret keys' do
context 'when none of the secrets exist' do
before do
stub_env('SECRET_KEY_BASE', nil)
@ -24,19 +27,29 @@ describe 'create_tokens', lib: true do
allow(self).to receive(:warn_missing_secret)
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
keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base)
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
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('otp_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
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['otp_key_base']).to eq(secrets.otp_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
create_tokens
@ -63,6 +77,7 @@ describe 'create_tokens', lib: true do
context 'when the other secrets all exist' do
before do
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(:read).with('.secret').and_return('file_key')
@ -73,6 +88,7 @@ describe 'create_tokens', lib: true do
stub_env('SECRET_KEY_BASE', 'env_key')
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
secrets.jws_private_key = 'jws_private_key'
end
it 'does not issue a warning' do
@ -98,6 +114,7 @@ describe 'create_tokens', lib: true do
before do
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
secrets.jws_private_key = 'jws_private_key'
end
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.otp_key_base).to eq('otp_key_base')
expect(secrets.db_key_base).to eq('db_key_base')
expect(secrets.jws_private_key).to eq('jws_private_key')
end
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['otp_key_base']).to eq('file_key')
expect(new_secrets['db_key_base']).to eq('db_key_base')
expect(new_secrets['jws_private_key']).to eq('jws_private_key')
end
create_tokens

View File

@ -3,6 +3,24 @@ require 'spec_helper'
describe Gitlab::Auth, lib: true do
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
context 'build token' do
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
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
before do
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)

View File

@ -34,4 +34,28 @@ describe PersonalAccessToken, models: true do
expect(active_personal_access_token).to be_active
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

View File

@ -39,4 +39,22 @@ describe API::API, api: true do
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

View File

@ -29,5 +29,27 @@ describe API::API, api: true do
expect(json_response['access_token']).not_to be_nil
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

View File

@ -87,5 +87,23 @@ describe API::Session, api: true do
expect(response).to have_http_status(400)
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

View File

@ -221,12 +221,20 @@ describe 'Git HTTP requests', lib: true do
end
context "when the user is blocked" do
it "responds with status 404" do
it "responds with status 401" do
user.block
project.team << [user, :master]
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

View 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

View 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