Commit 921b2fe0 authored by Alex Kalderimis's avatar Alex Kalderimis Committed by Imre Farkas

Pass gl_auth_type as part of devise OmniAuth flow

This adds a new communication flow for OmniAuth logins between
GitLab instances. When we set up the application, we attempt to detect
if the appliation is being used for authentication (via a query
parameter), and if so, enforce the use of a restricted scope, preventing
login apps gaining full API access.
parent cad3fb53
......@@ -3,6 +3,7 @@
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
include Gitlab::Experimentation::ControllerConcern
include InitializesCurrentUserMode
include Gitlab::Utils::StrongMemoize
before_action :verify_confirmed_email!, :verify_confidential_application!
......@@ -27,6 +28,56 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
private
def pre_auth_params
# Cannot be achieved with a before_action hook, due to the execution order.
downgrade_scopes! if action_name == 'new'
super
end
# limit scopes when signing in with GitLab
def downgrade_scopes!
return unless Feature.enabled?(:omniauth_login_minimal_scopes, current_user,
default_enabled: :yaml)
auth_type = params.delete('gl_auth_type')
return unless auth_type == 'login'
ensure_read_user_scope!
params['scope'] = Gitlab::Auth::READ_USER_SCOPE.to_s if application_has_read_user_scope?
end
# Configure the application to support read_user scope, if it already
# supports scopes with greater levels of privileges.
def ensure_read_user_scope!
return if application_has_read_user_scope?
return unless application_has_api_scope?
add_read_user_scope!
end
def add_read_user_scope!
return unless doorkeeper_application
scopes = doorkeeper_application.scopes
scopes.add(Gitlab::Auth::READ_USER_SCOPE)
doorkeeper_application.scopes = scopes
doorkeeper_application.save!
end
def doorkeeper_application
strong_memoize(:doorkeeper_application) { ::Doorkeeper::OAuth::Client.find(params['client_id'])&.application }
end
def application_has_read_user_scope?
doorkeeper_application&.includes_scope?(Gitlab::Auth::READ_USER_SCOPE)
end
def application_has_api_scope?
doorkeeper_application&.includes_scope?(*::Gitlab::Auth::API_SCOPES)
end
# Confidential apps require the client_secret to be sent with the request.
# Doorkeeper allows implicit grant flow requests (response_type=token) to
# work without client_secret regardless of the confidential setting.
......
---
name: omniauth_login_minimal_scopes
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78556
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351331
milestone: '14.8'
type: development
group: 'group::authentication and authorization'
default_enabled: false
......@@ -24,7 +24,12 @@ GitLab.com generates an application ID and secret key for you to use.
http://your-gitlab.example.com/users/auth/gitlab/callback
```
The first link is required for the importer and second for the authorization.
The first link is required for the importer and second for authentication.
If you:
- Plan to use the importer, you can leave scopes as they are.
- Only want to use this application for authentication, we recommend using a more minimal set of scopes. `read_user` is sufficient.
1. Select **Save application**.
1. You should now see an **Application ID** and **Secret**. Keep this page open as you continue
......@@ -57,7 +62,9 @@ GitLab.com generates an application ID and secret key for you to use.
# label: "Provider name", # optional label for login button, defaults to "GitLab.com"
app_id: "YOUR_APP_ID",
app_secret: "YOUR_APP_SECRET",
args: { scope: "api" }
args: { scope: "read_user" # optional: defaults to the scopes of the application
, client_options: { site: "https://gitlab.example.com/api/v4" }
}
}
]
```
......@@ -71,7 +78,8 @@ GitLab.com generates an application ID and secret key for you to use.
label: "Provider name", # optional label for login button, defaults to "GitLab.com"
app_id: "YOUR_APP_ID",
app_secret: "YOUR_APP_SECRET",
args: { scope: "api", client_options: { site: "https://gitlab.example.com/api/v4" } }
args: { scope: "read_user" # optional: defaults to the scopes of the application
, client_options: { site: "https://gitlab.example.com/api/v4" } }
}
]
```
......@@ -83,7 +91,7 @@ GitLab.com generates an application ID and secret key for you to use.
# label: 'Provider name', # optional label for login button, defaults to "GitLab.com"
app_id: 'YOUR_APP_ID',
app_secret: 'YOUR_APP_SECRET',
args: { scope: 'api' } }
args: { "client_options": { "site": 'https://gitlab.example.com/api/v4' } }
```
Or, for installations from source to authenticate against a different GitLab instance:
......@@ -93,7 +101,7 @@ GitLab.com generates an application ID and secret key for you to use.
label: 'Provider name', # optional label for login button, defaults to "GitLab.com"
app_id: 'YOUR_APP_ID',
app_secret: 'YOUR_APP_SECRET',
args: { scope: 'api', "client_options": { "site": 'https://gitlab.example.com/api/v4' } }
args: { "client_options": { "site": 'https://gitlab.example.com/api/v4' } }
```
1. Change `'YOUR_APP_ID'` to the Application ID from the GitLab.com application page.
......@@ -101,7 +109,6 @@ GitLab.com generates an application ID and secret key for you to use.
1. Save the configuration file.
1. Based on how GitLab was installed, implement these changes by using
the appropriate method:
- Omnibus GitLab: [reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure).
- Source: [restart GitLab](../administration/restart_gitlab.md#installations-from-source).
......@@ -110,3 +117,20 @@ regular sign-in form. Select the icon to begin the authentication process.
GitLab.com asks the user to sign in and authorize the GitLab application. If
everything goes well, the user is returned to your GitLab instance and is
signed in.
## Reduce access privileges on sign in
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/337663) in GitLab 14.8 [with a flag](../administration/feature_flags.md) named `omniauth_login_minimal_scopes`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `omniauth_login_minimal_scopes`. On GitLab.com, this feature is not available.
If you use a GitLab instance for authentication, you can reduce access rights when an OAuth application is used for sign in.
Any OAuth application can advertise the purpose of the application with the
authorization parameter: `gl_auth_type=login`. If the application is
configured with `api` or `read_api`, the access token is issued with
`read_user` for login, because no higher permissions are needed.
The GitLab OAuth client is configured to pass this parameter, but other
applications can also pass it.
......@@ -35,12 +35,15 @@ is select the `openid` scope in the application settings.
## Settings discovery
If your client allows importing OIDC settings from a discovery URL, you can use the following URL to automatically find the correct settings:
If your client allows importing OIDC settings from a discovery URL, you can use
the following URL to automatically find the correct settings for GitLab.com:
```plaintext
https://gitlab.example.com/.well-known/openid-configuration
https://gitlab.com/.well-known/openid-configuration
```
Similar URLs can be used for other GitLab instances.
## Shared information
The following user information is shared with clients:
......
# frozen_string_literal: true
# Encapsulate a scope used for authorization, such as `api`, or `read_user`
# See Gitlab::Auth for the set of available scopes, and their purposes.
module API
class Scope
attr_reader :name, :if
......
......@@ -6,25 +6,35 @@ module Gitlab
IpBlacklisted = Class.new(StandardError)
# Scopes used for GitLab API access
API_SCOPES = [:api, :read_user, :read_api].freeze
API_SCOPE = :api
READ_API_SCOPE = :read_api
READ_USER_SCOPE = :read_user
API_SCOPES = [API_SCOPE, READ_API_SCOPE, READ_USER_SCOPE].freeze
PROFILE_SCOPE = :profile
EMAIL_SCOPE = :email
OPENID_SCOPE = :openid
# Scopes used for OpenID Connect
OPENID_SCOPES = [OPENID_SCOPE].freeze
# OpenID Connect profile scopes
PROFILE_SCOPES = [PROFILE_SCOPE, EMAIL_SCOPE].freeze
# Scopes used for GitLab Repository access
REPOSITORY_SCOPES = [:read_repository, :write_repository].freeze
READ_REPOSITORY_SCOPE = :read_repository
WRITE_REPOSITORY_SCOPE = :write_repository
REPOSITORY_SCOPES = [READ_REPOSITORY_SCOPE, WRITE_REPOSITORY_SCOPE].freeze
# Scopes used for GitLab Docker Registry access
REGISTRY_SCOPES = [:read_registry, :write_registry].freeze
READ_REGISTRY_SCOPE = :read_registry
WRITE_REGISTRY_SCOPE = :write_registry
REGISTRY_SCOPES = [READ_REGISTRY_SCOPE, WRITE_REGISTRY_SCOPE].freeze
# Scopes used for GitLab as admin
ADMIN_SCOPES = [:sudo].freeze
# Scopes used for OpenID Connect
OPENID_SCOPES = [:openid].freeze
# OpenID Connect profile scopes
PROFILE_SCOPES = [:profile, :email].freeze
SUDO_SCOPE = :sudo
ADMIN_SCOPES = [SUDO_SCOPE].freeze
# Default scopes for OAuth applications that don't define their own
DEFAULT_SCOPES = [:api].freeze
DEFAULT_SCOPES = [API_SCOPE].freeze
CI_JOB_USER = 'gitlab-ci-token'
......
......@@ -28,6 +28,10 @@ module Gitlab
{ fail_with_empty_uid: true }
when 'google_oauth2'
{ client_options: { connection_opts: { request: { timeout: OAUTH2_TIMEOUT_SECONDS } } } }
when 'gitlab'
{
authorize_params: { gl_auth_type: 'login' }
}
else
{}
end
......
......@@ -4,7 +4,13 @@ require 'spec_helper'
RSpec.describe Oauth::AuthorizationsController do
let(:user) { create(:user) }
let!(:application) { create(:oauth_application, scopes: 'api read_user', redirect_uri: 'http://example.com') }
let(:application_scopes) { 'api read_user' }
let!(:application) do
create(:oauth_application, scopes: application_scopes,
redirect_uri: 'http://example.com')
end
let(:params) do
{
response_type: "code",
......@@ -119,6 +125,92 @@ RSpec.describe Oauth::AuthorizationsController do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/redirect')
end
context 'with gl_auth_type=login' do
let(:minimal_scope) { Gitlab::Auth::READ_USER_SCOPE.to_s }
before do
params[:gl_auth_type] = 'login'
end
shared_examples 'downgrades scopes' do
it 'downgrades the scopes' do
subject
pre_auth = controller.send(:pre_auth)
expect(pre_auth.scopes).to contain_exactly(minimal_scope)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/new')
# See: config/locales/doorkeeper.en.yml
expect(response.body).to include("Read the authenticated user&#39;s personal information")
expect(response.body).not_to include("Access the authenticated user&#39;s API")
end
end
shared_examples 'adds read_user scope' do
it 'modifies the client.application.scopes' do
expect { subject }
.to change { application.reload.scopes }.to include(minimal_scope)
end
it 'does not remove pre-existing scopes' do
subject
expect(application.scopes).to include(*application_scopes.split(/ /))
end
end
context 'the application has all scopes' do
let(:application_scopes) { 'api read_api read_user' }
include_examples 'downgrades scopes'
end
context 'the application has api and read_user scopes' do
let(:application_scopes) { 'api read_user' }
include_examples 'downgrades scopes'
end
context 'the application has read_api and read_user scopes' do
let(:application_scopes) { 'read_api read_user' }
include_examples 'downgrades scopes'
end
context 'the application has only api scopes' do
let(:application_scopes) { 'api' }
include_examples 'downgrades scopes'
include_examples 'adds read_user scope'
end
context 'the application has only read_api scopes' do
let(:application_scopes) { 'read_api' }
include_examples 'downgrades scopes'
include_examples 'adds read_user scope'
end
context 'the application has scopes we do not handle' do
let(:application_scopes) { Gitlab::Auth::PROFILE_SCOPE.to_s }
before do
params[:scope] = application_scopes
end
it 'does not modify the scopes' do
subject
pre_auth = controller.send(:pre_auth)
expect(pre_auth.scopes).to contain_exactly(application_scopes)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/new')
end
end
end
end
end
end
......
......@@ -10,29 +10,29 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do
expect(subject::API_SCOPES).to eq %i[api read_user read_api]
expect(subject::API_SCOPES).to match_array %i[api read_user read_api]
end
it 'ADMIN_SCOPES contains all scopes for ADMIN access' do
expect(subject::ADMIN_SCOPES).to eq %i[sudo]
expect(subject::ADMIN_SCOPES).to match_array %i[sudo]
end
it 'REPOSITORY_SCOPES contains all scopes for REPOSITORY access' do
expect(subject::REPOSITORY_SCOPES).to eq %i[read_repository write_repository]
expect(subject::REPOSITORY_SCOPES).to match_array %i[read_repository write_repository]
end
it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
expect(subject::OPENID_SCOPES).to eq [:openid]
expect(subject::OPENID_SCOPES).to match_array [:openid]
end
it 'DEFAULT_SCOPES contains all default scopes' do
expect(subject::DEFAULT_SCOPES).to eq [:api]
expect(subject::DEFAULT_SCOPES).to match_array [:api]
end
it 'optional_scopes contains all non-default scopes' do
stub_container_registry_config(enabled: true)
expect(subject.optional_scopes).to eq %i[read_user read_api read_repository write_repository read_registry write_registry sudo openid profile email]
expect(subject.optional_scopes).to match_array %i[read_user read_api read_repository write_repository read_registry write_registry sudo openid profile email]
end
end
......@@ -40,21 +40,21 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'contains all non-default scopes' do
stub_container_registry_config(enabled: true)
expect(subject.all_available_scopes).to eq %i[api read_user read_api read_repository write_repository read_registry write_registry sudo]
expect(subject.all_available_scopes).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo]
end
it 'contains for non-admin user all non-default scopes without ADMIN access' do
stub_container_registry_config(enabled: true)
user = create(:user, admin: false)
expect(subject.available_scopes_for(user)).to eq %i[api read_user read_api read_repository write_repository read_registry write_registry]
expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry]
end
it 'contains for admin user all non-default scopes with ADMIN access' do
stub_container_registry_config(enabled: true)
user = create(:user, admin: true)
expect(subject.available_scopes_for(user)).to eq %i[api read_user read_api read_repository write_repository read_registry write_registry sudo]
expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo]
end
context 'registry_scopes' do
......
......@@ -101,5 +101,19 @@ RSpec.describe Gitlab::OmniauthInitializer do
subject.execute([google_config])
end
it 'configures defaults for gitlab' do
conf = {
'name' => 'gitlab',
"args" => {}
}
expect(devise_config).to receive(:omniauth).with(
:gitlab,
authorize_params: { gl_auth_type: 'login' }
)
subject.execute([conf])
end
end
end
......@@ -275,7 +275,7 @@ RSpec.describe 'OpenID Connect requests' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issuer']).to eq('http://localhost')
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
expect(json_response['scopes_supported']).to eq(%w[api read_user read_api read_repository write_repository sudo openid profile email])
expect(json_response['scopes_supported']).to match_array %w[api read_user read_api read_repository write_repository sudo openid profile email]
end
context 'with a cross-origin request' do
......@@ -285,7 +285,7 @@ RSpec.describe 'OpenID Connect requests' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issuer']).to eq('http://localhost')
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
expect(json_response['scopes_supported']).to eq(%w[api read_user read_api read_repository write_repository sudo openid profile email])
expect(json_response['scopes_supported']).to match_array %w[api read_user read_api read_repository write_repository sudo openid profile email]
end
it_behaves_like 'cross-origin GET request'
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment