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

View file

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

View file

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

View file

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

View file

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

View file

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

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 :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application' belongs_to :application, class_name: 'Doorkeeper::Application'
end end

View file

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

View file

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

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

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 = { 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

View file

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

View file

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

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", ["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

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

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

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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