Commit 56168614 authored by Stan Hu's avatar Stan Hu

Merge branch 'if-212312-forti_authenticator' into 'master'

Add FortiAuthenticator as OTP method

See merge request gitlab-org/gitlab!45055
parents 35a7c3a3 b34f3e78
......@@ -67,7 +67,10 @@ class Admin::SessionsController < ApplicationController
end
def valid_otp_attempt?(user)
valid_otp_attempt = user.validate_and_consume_otp!(user_params[:otp_attempt])
otp_validation_result =
::Users::ValidateOtpService.new(user).execute(user_params[:otp_attempt])
valid_otp_attempt = otp_validation_result[:status] == :success
return valid_otp_attempt if Gitlab::Database.read_only?
valid_otp_attempt || user.invalidate_otp_backup_code!(user_params[:otp_attempt])
......
......@@ -47,7 +47,10 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
def create
if current_user.validate_and_consume_otp!(params[:pin_code])
otp_validation_result =
::Users::ValidateOtpService.new(current_user).execute(params[:pin_code])
if otp_validation_result[:status] == :success
ActiveSession.destroy_all_but_current(current_user, session)
Users::UpdateService.new(current_user, user: current_user, otp_required_for_login: true).execute! do |user|
......
......@@ -264,7 +264,10 @@ class SessionsController < Devise::SessionsController
end
def valid_otp_attempt?(user)
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
otp_validation_result =
::Users::ValidateOtpService.new(user).execute(user_params[:otp_attempt])
return true if otp_validation_result[:status] == :success
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
end
......
......@@ -793,7 +793,7 @@ class User < ApplicationRecord
end
def two_factor_otp_enabled?
otp_required_for_login?
otp_required_for_login? || Feature.enabled?(:forti_authenticator, self)
end
def two_factor_u2f_enabled?
......
# frozen_string_literal: true
module Users
class ValidateOtpService < BaseService
def initialize(current_user)
@current_user = current_user
@strategy = if Feature.enabled?(:forti_authenticator, current_user)
::Gitlab::Auth::Otp::Strategies::FortiAuthenticator.new(current_user)
else
::Gitlab::Auth::Otp::Strategies::Devise.new(current_user)
end
end
def execute(otp_code)
strategy.validate(otp_code)
rescue StandardError => ex
Gitlab::ErrorTracking.log_exception(ex)
error(message: ex.message)
end
private
attr_reader :strategy
end
end
---
name: forti_authenticator
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45055
rollout_issue_url:
type: development
group: group::access
default_enabled: false
......@@ -1022,6 +1022,21 @@ production: &base
# cas3:
# session_duration: 28800
# FortiAuthenticator settings
forti_authenticator:
# Allow using FortiAuthenticator as OTP provider
enabled: false
# Host and port of FortiAuthenticator instance
# host: forti_authenticator.example.com
# port: 443
# Username for accessing FortiAuthenticator API
# username: john
# Access token for FortiAuthenticator API
# access_token: 123s3cr3t456
# Shared file storage settings
shared:
# path: /mnt/gitlab # Default: shared
......
......@@ -766,6 +766,13 @@ Gitlab.ee do
Settings.smartcard['san_extensions'] = false if Settings.smartcard['san_extensions'].nil?
end
#
# FortiAuthenticator
#
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
#
# Extra customization
#
......
# frozen_string_literal: true
module Gitlab
module Auth
module Otp
module Strategies
class Base
def initialize(user)
@user = user
end
private
attr_reader :user
def success
{ status: :success }
end
def error(message, http_status = nil)
result = { message: message,
status: :error }
result[:http_status] = http_status if http_status
result
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Auth
module Otp
module Strategies
class Devise < Base
def validate(otp_code)
user.validate_and_consume_otp!(otp_code) ? success : error('invalid OTP code')
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Auth
module Otp
module Strategies
class FortiAuthenticator < Base
def validate(otp_code)
body = { username: user.username,
token_code: otp_code }
response = Gitlab::HTTP.post(
auth_url,
headers: { 'Content-Type': 'application/json' },
body: body.to_json,
basic_auth: api_credentials)
# 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)
end
private
def auth_url
host = ::Gitlab.config.forti_authenticator.host
port = ::Gitlab.config.forti_authenticator.port
path = 'api/v1/auth/'
"https://#{host}:#{port}/#{path}"
end
def api_credentials
{ username: ::Gitlab.config.forti_authenticator.username,
password: ::Gitlab.config.forti_authenticator.token }
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Auth::Otp::Strategies::Devise do
let_it_be(:user) { create(:user) }
let(:otp_code) { 42 }
subject(:validate) { described_class.new(user).validate(otp_code) }
it 'calls Devise' do
expect(user).to receive(:validate_and_consume_otp!).with(otp_code)
validate
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do
let_it_be(:user) { create(:user) }
let(:otp_code) { 42 }
let(:host) { 'forti_authenticator.example.com' }
let(:port) { '444' }
let(:api_username) { 'janedoe' }
let(:api_token) { 's3cr3t' }
let(:forti_authenticator_auth_url) { "https://#{host}:#{port}/api/v1/auth/" }
subject(:validate) { described_class.new(user).validate(otp_code) }
before do
stub_feature_flags(forti_authenticator: true)
stub_forti_authenticator_config(
host: host,
port: port,
username: api_username,
token: api_token
)
request_body = { username: user.username,
token_code: otp_code }
stub_request(:post, forti_authenticator_auth_url)
.with(body: JSON(request_body), headers: { 'Content-Type' => 'application/json' })
.to_return(status: response_status, body: '', headers: {})
end
context 'successful validation' do
let(:response_status) { 200 }
it 'returns success' do
expect(validate[:status]).to eq(:success)
end
end
context 'unsuccessful validation' do
let(:response_status) { 401 }
it 'returns error' do
expect(validate[:status]).to eq(:error)
end
end
def stub_forti_authenticator_config(forti_authenticator_settings)
allow(::Gitlab.config.forti_authenticator).to(receive_messages(forti_authenticator_settings))
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Users::ValidateOtpService do
let_it_be(:user) { create(:user) }
let(:otp_code) { 42 }
subject(:validate) { described_class.new(user).execute(otp_code) }
context 'Devise' do
it 'calls Devise strategy' do
expect_next_instance_of(::Gitlab::Auth::Otp::Strategies::Devise) do |strategy|
expect(strategy).to receive(:validate).with(otp_code).once
end
validate
end
end
context 'FortiAuthenticator' do
before do
stub_feature_flags(forti_authenticator: true)
end
it 'calls FortiAuthenticator strategy' do
expect_next_instance_of(::Gitlab::Auth::Otp::Strategies::FortiAuthenticator) do |strategy|
expect(strategy).to receive(:validate).with(otp_code).once
end
validate
end
end
end
......@@ -212,6 +212,10 @@ RSpec.configure do |config|
# for now whilst we migrate as much as we can over the GraphQL
stub_feature_flags(merge_request_widget_graphql: false)
# Using FortiAuthenticator as OTP provider is disabled by default in
# tests, until we introduce it in user settings
stub_feature_flags(forti_authenticator: 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