Merge branch 'siemens/gitlab-ce-feature/openid-connect'
This commit is contained in:
commit
de37dcee90
1
Gemfile
1
Gemfile
|
@ -20,6 +20,7 @@ gem 'rugged', '~> 0.24.0'
|
|||
# Authentication libraries
|
||||
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'
|
||||
|
|
13
Gemfile.lock
13
Gemfile.lock
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 :application, class_name: 'Doorkeeper::Application'
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
resource_owner_authenticator do
|
||||
# Put your resource owner authentication logic here.
|
||||
if current_user
|
||||
current_user
|
||||
else
|
||||
# Ensure user is redirected to redirect_uri after login
|
||||
session[:user_return_to] = request.fullpath
|
||||
current_user || redirect_to(new_user_session_url)
|
||||
redirect_to(new_user_session_url)
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
resource_owner_from_credentials do |routes|
|
||||
|
|
|
@ -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 = {
|
||||
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
|
||||
|
|
|
@ -60,6 +60,7 @@ en:
|
|||
scopes:
|
||||
api: Access your API
|
||||
read_user: Read user information
|
||||
openid: Authenticate using OpenID Connect
|
||||
|
||||
flash:
|
||||
applications:
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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", ["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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
resource_owner
|
||||
application
|
||||
token '123456'
|
||||
token { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
|
||||
scopes { application.scopes }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 }
|
||||
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 New Issue