Commit c6cdc324 authored by Imre Farkas's avatar Imre Farkas

Merge branch '212313-integrate-forticloud-token-to-gitlab-2fa-options' into 'master'

Resolve "Integrate FortiToken Cloud to GitLab 2FA options"

See merge request gitlab-org/gitlab!49089
parents bb38153e 317dc0af
......@@ -25,6 +25,7 @@ class User < ApplicationRecord
include IgnorableColumns
include UpdateHighestRole
include HasUserType
include Gitlab::Auth::Otp::Fortinet
DEFAULT_NOTIFICATION_LEVEL = :participating
......@@ -810,7 +811,9 @@ class User < ApplicationRecord
end
def two_factor_otp_enabled?
otp_required_for_login? || Feature.enabled?(:forti_authenticator, self)
otp_required_for_login? ||
forti_authenticator_enabled?(self) ||
forti_token_cloud_enabled?(self)
end
def two_factor_u2f_enabled?
......
......@@ -2,10 +2,14 @@
module Users
class ValidateOtpService < BaseService
include ::Gitlab::Auth::Otp::Fortinet
def initialize(current_user)
@current_user = current_user
@strategy = if Feature.enabled?(:forti_authenticator, current_user)
@strategy = if forti_authenticator_enabled?(current_user)
::Gitlab::Auth::Otp::Strategies::FortiAuthenticator.new(current_user)
elsif forti_token_cloud_enabled?(current_user)
::Gitlab::Auth::Otp::Strategies::FortiTokenCloud.new(current_user)
else
::Gitlab::Auth::Otp::Strategies::Devise.new(current_user)
end
......
---
name: forti_token_cloud
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49089
rollout_issue_url:
milestone: '13.7'
type: development
group: group::access
default_enabled: false
......@@ -1042,6 +1042,15 @@ production: &base
# Access token for FortiAuthenticator API
# access_token: 123s3cr3t456
# FortiToken Cloud settings
forti_token_cloud:
# Allow using FortiToken Cloud as OTP provider
enabled: false
# Client ID and Secret to access FortiToken Cloud API
# client_id: 'YOUR_FORTI_TOKEN_CLOUD_CLIENT_ID'
# client_secret: 'YOUR_FORTI_TOKEN_CLOUD_CLIENT_SECRET'
# Shared file storage settings
shared:
# path: /mnt/gitlab # Default: shared
......
......@@ -791,6 +791,12 @@ Settings['forti_authenticator'] ||= Settingslogic.new({})
Settings.forti_authenticator['enabled'] = false if Settings.forti_authenticator['enabled'].nil?
Settings.forti_authenticator['port'] = 443 if Settings.forti_authenticator['port'].to_i == 0
#
# FortiToken Cloud
#
Settings['forti_token_cloud'] ||= Settingslogic.new({})
Settings.forti_token_cloud['enabled'] = false if Settings.forti_token_cloud['enabled'].nil?
#
# Extra customization
#
......
......@@ -142,6 +142,86 @@ to run the following command:
Feature.enable(:forti_authenticator, User.find(<user ID>))
```
### One-time password via FortiToken Cloud
> - Introduced in [GitLab 13.7](https://gitlab.com/gitlab-org/gitlab/-/issues/212313).
> - It's deployed behind a feature flag, disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-fortitoken-cloud-integration).
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
You can use FortiToken Cloud as an OTP provider in GitLab. Users must exist in
both FortiToken Cloud and GitLab with the exact same username, and users must
have FortiToken configured in FortiToken Cloud.
You'll also need a `client_id` and `client_secret` to configure FortiToken Cloud.
To get these, see the `REST API Guide` at
[`Fortinet Document Library`](https://docs.fortinet.com/document/fortitoken-cloud/20.4.d/rest-api).
First configure FortiToken Cloud in GitLab. On your GitLab server:
1. Open the configuration file.
For Omnibus GitLab:
```shell
sudo editor /etc/gitlab/gitlab.rb
```
For installations from source:
```shell
cd /home/git/gitlab
sudo -u git -H editor config/gitlab.yml
```
1. Add the provider configuration:
For Omnibus package:
```ruby
gitlab_rails['forti_token_cloud_enabled'] = true
gitlab_rails['forti_token_cloud_client_id'] = '<your_fortinet_cloud_client_id>'
gitlab_rails['forti_token_cloud_client_secret'] = '<your_fortinet_cloud_client_secret>'
```
For installations from source:
```yaml
forti_token_cloud:
enabled: true
client_id: YOUR_FORTI_TOKEN_CLOUD_CLIENT_ID
client_secret: YOUR_FORTI_TOKEN_CLOUD_CLIENT_SECRET
```
1. Save the configuration file.
1. [Reconfigure](../../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure)
or [restart GitLab](../../../administration/restart_gitlab.md#installations-from-source)
for the changes to take effect if you installed GitLab via Omnibus or from
source respectively.
#### Enable or disable FortiToken Cloud integration
FortiToken Cloud integration is under development and not ready for production use.
It is deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:forti_token_cloud, User.find(<user ID>))
```
To disable it:
```ruby
Feature.disable(:forti_token_cloud, User.find(<user ID>))
```
### U2F device
> Introduced in [GitLab 8.9](https://about.gitlab.com/blog/2016/06/22/gitlab-adds-support-for-u2f/).
......
# frozen_string_literal: true
module Gitlab
module Auth
module Otp
module Fortinet
private
def forti_authenticator_enabled?(user)
::Gitlab.config.forti_authenticator.enabled &&
Feature.enabled?(:forti_authenticator, user)
end
def forti_token_cloud_enabled?(user)
::Gitlab.config.forti_token_cloud.enabled &&
Feature.enabled?(:forti_token_cloud, user)
end
end
end
end
end
......@@ -25,6 +25,10 @@ module Gitlab
result
end
def error_from_response(response)
error(response.message, response.code)
end
end
end
end
......
......@@ -17,7 +17,7 @@ module Gitlab
# Successful authentication results in HTTP 200: OK
# https://docs.fortinet.com/document/fortiauthenticator/6.2.0/rest-api-solution-guide/704555/authentication-auth
response.ok? ? success : error(message: response.message, http_status: response.code)
response.ok? ? success : error_from_response(response)
end
private
......
# frozen_string_literal: true
module Gitlab
module Auth
module Otp
module Strategies
class FortiTokenCloud < Base
include Gitlab::Utils::StrongMemoize
BASE_API_URL = 'https://ftc.fortinet.com:9696/api/v1'
def validate(otp_code)
if access_token_create_response.created?
otp_verification_response = verify_otp(otp_code)
otp_verification_response.ok? ? success : error_from_response(otp_verification_response)
else
error_from_response(access_token_create_response)
end
end
private
# TODO: Cache the access token: https://gitlab.com/gitlab-org/gitlab/-/issues/292437
def access_token_create_response
# Returns '201 CREATED' on successful creation of a new access token.
strong_memoize(:access_token_create_response) do
post(
url: url('/login'),
body: {
client_id: ::Gitlab.config.forti_token_cloud.client_id,
client_secret: ::Gitlab.config.forti_token_cloud.client_secret
}.to_json
)
end
end
def access_token
Gitlab::Json.parse(access_token_create_response)['access_token']
end
def verify_otp(otp_code)
# Returns '200 OK' on successful verification.
# Uses the access token created via `access_token_create_response` as the auth token.
post(
url: url('/auth'),
headers: { 'Authorization': "Bearer #{access_token}" },
body: {
username: user.username,
token: otp_code
}.to_json
)
end
def url(path)
BASE_API_URL + path
end
def post(url:, body:, headers: {})
Gitlab::HTTP.post(
url,
headers: {
'Content-Type': 'application/json'
}.merge(headers),
body: body,
verify: false # FTC API Docs specifically mentions to turn off SSL Verification while making requests.
)
end
end
end
end
end
end
......@@ -16,9 +16,10 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do
subject(:validate) { described_class.new(user).validate(otp_code) }
before do
stub_feature_flags(forti_authenticator: true)
stub_feature_flags(forti_authenticator: user)
stub_forti_authenticator_config(
enabled: true,
host: host,
port: port,
username: api_username,
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do
let_it_be(:user) { create(:user) }
let(:otp_code) { 42 }
let(:url) { 'https://ftc.example.com:9696/api/v1' }
let(:client_id) { 'client_id' }
let(:client_secret) { 's3cr3t' }
let(:access_token_create_url) { url + '/login' }
let(:otp_verification_url) { url + '/auth' }
let(:access_token) { 'an_access_token' }
let(:access_token_create_response_body) { '' }
subject(:validate) { described_class.new(user).validate(otp_code) }
before do
stub_feature_flags(forti_token_cloud: user)
stub_const("#{described_class}::BASE_API_URL", url)
stub_forti_token_cloud_config(
enabled: true,
client_id: client_id,
client_secret: client_secret
)
access_token_request_body = { client_id: client_id,
client_secret: client_secret }
stub_request(:post, access_token_create_url)
.with(body: JSON(access_token_request_body), headers: { 'Content-Type' => 'application/json' })
.to_return(
status: access_token_create_response_status,
body: Gitlab::Json.generate(access_token_create_response_body),
headers: {}
)
end
context 'access token is created successfully' do
let(:access_token_create_response_body) { { access_token: access_token, expires_in: 3600 } }
let(:access_token_create_response_status) { 201 }
before do
otp_verification_request_body = { username: user.username,
token: otp_code }
stub_request(:post, otp_verification_url)
.with(body: JSON(otp_verification_request_body),
headers: {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{access_token}"
})
.to_return(status: otp_verification_response_status, body: '', headers: {})
end
context 'otp verification is successful' do
let(:otp_verification_response_status) { 200 }
it 'returns success' do
expect(validate[:status]).to eq(:success)
end
end
context 'otp verification is not successful' do
let(:otp_verification_response_status) { 401 }
it 'returns error' do
expect(validate[:status]).to eq(:error)
end
end
end
context 'access token creation fails' do
let(:access_token_create_response_status) { 400 }
it 'returns error' do
expect(validate[:status]).to eq(:error)
end
end
def stub_forti_token_cloud_config(forti_token_cloud_settings)
allow(::Gitlab.config.forti_token_cloud).to(receive_messages(forti_token_cloud_settings))
end
end
......@@ -1585,6 +1585,80 @@ RSpec.describe User do
end
end
describe '#two_factor_otp_enabled?' do
let_it_be(:user) { create(:user) }
context 'when 2FA is enabled by an MFA Device' do
let(:user) { create(:user, :two_factor) }
it { expect(user.two_factor_otp_enabled?).to eq(true) }
end
context 'FortiAuthenticator' do
context 'when enabled via GitLab settings' do
before do
allow(::Gitlab.config.forti_authenticator).to receive(:enabled).and_return(true)
end
context 'when feature is disabled for the user' do
before do
stub_feature_flags(forti_authenticator: false)
end
it { expect(user.two_factor_otp_enabled?).to eq(false) }
end
context 'when feature is enabled for the user' do
before do
stub_feature_flags(forti_authenticator: user)
end
it { expect(user.two_factor_otp_enabled?).to eq(true) }
end
end
context 'when disabled via GitLab settings' do
before do
allow(::Gitlab.config.forti_authenticator).to receive(:enabled).and_return(false)
end
it { expect(user.two_factor_otp_enabled?).to eq(false) }
end
end
context 'FortiTokenCloud' do
context 'when enabled via GitLab settings' do
before do
allow(::Gitlab.config.forti_token_cloud).to receive(:enabled).and_return(true)
end
context 'when feature is disabled for the user' do
before do
stub_feature_flags(forti_token_cloud: false)
end
it { expect(user.two_factor_otp_enabled?).to eq(false) }
end
context 'when feature is enabled for the user' do
before do
stub_feature_flags(forti_token_cloud: user)
end
it { expect(user.two_factor_otp_enabled?).to eq(true) }
end
end
context 'when disabled via GitLab settings' do
before do
allow(::Gitlab.config.forti_token_cloud).to receive(:enabled).and_return(false)
end
it { expect(user.two_factor_otp_enabled?).to eq(false) }
end
end
end
describe 'projects' do
before do
@user = create(:user)
......
......@@ -20,7 +20,8 @@ RSpec.describe Users::ValidateOtpService do
context 'FortiAuthenticator' do
before do
stub_feature_flags(forti_authenticator: true)
stub_feature_flags(forti_authenticator: user)
allow(::Gitlab.config.forti_authenticator).to receive(:enabled).and_return(true)
end
it 'calls FortiAuthenticator strategy' do
......@@ -31,4 +32,19 @@ RSpec.describe Users::ValidateOtpService do
validate
end
end
context 'FortiTokenCloud' do
before do
stub_feature_flags(forti_token_cloud: user)
allow(::Gitlab.config.forti_token_cloud).to receive(:enabled).and_return(true)
end
it 'calls FortiTokenCloud strategy' do
expect_next_instance_of(::Gitlab::Auth::Otp::Strategies::FortiTokenCloud) do |strategy|
expect(strategy).to receive(:validate).with(otp_code).once
end
validate
end
end
end
......@@ -230,6 +230,10 @@ RSpec.configure do |config|
# tests, until we introduce it in user settings
stub_feature_flags(forti_authenticator: false)
# Using FortiToken Cloud as OTP provider is disabled by default in
# tests, until we introduce it in user settings
stub_feature_flags(forti_token_cloud: false)
enable_rugged = example.metadata[:enable_rugged].present?
# Disable Rugged features by default
......
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