Skip to content

Commit

Permalink
[api] Add initial support for authenticating users via Kerberos.
Browse files Browse the repository at this point in the history
  • Loading branch information
Hemmo Nieminen authored and bgeuken committed Apr 6, 2017
1 parent 0523386 commit 8f8c507
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 17 deletions.
2 changes: 2 additions & 0 deletions src/api/Gemfile
Expand Up @@ -74,6 +74,8 @@ group :development, :production do
gem 'clockwork', '>= 0.7'
# as interface to LDAP
gem 'ruby-ldap', require: false
# to enable GSSAPI / Kerberos authentication
gem "gssapi", require: false
end

group :production do
Expand Down
6 changes: 5 additions & 1 deletion src/api/Gemfile.lock
Expand Up @@ -116,16 +116,19 @@ GEM
faker (1.7.3)
i18n (~> 0.5)
feature (1.4.0)
flog (4.6.1)
ffi (1.9.17)
path_expander (~> 1.0)
ruby_parser (~> 3.1, > 3.1.0)
sexp_processor (~> 4.8)
flog (4.6.1)
flot-rails (0.0.7)
jquery-rails
font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1)
globalid (0.3.7)
activesupport (>= 4.1.0)
gssapi (1.2.0)
ffi (>= 1.0.1)
haml (4.0.7)
tilt
haml_lint (0.23.2)
Expand Down Expand Up @@ -398,6 +401,7 @@ DEPENDENCIES
flog (> 4.1.0)
flot-rails
font-awesome-rails
gssapi
haml
haml_lint
hoptoad_notifier (~> 2.3)
Expand Down
10 changes: 8 additions & 2 deletions src/api/app/controllers/application_controller.rb
Expand Up @@ -37,7 +37,7 @@ class ApplicationController < ActionController::Base
to: :authenticator

def authenticator
@authenticator ||= Authenticator.new(request, session)
@authenticator ||= Authenticator.new(request, session, response)
end

def pundit_user
Expand Down Expand Up @@ -303,7 +303,13 @@ def gather_exception_defaults(opt)
end

if @status == 401
response.headers["WWW-Authenticate"] = 'basic realm="API login"'
unless response.headers["WWW-Authenticate"]
if CONFIG['kerberos_service_principal']
response.headers["WWW-Authenticate"] = 'Negotiate, basic realm="API login"'
else
response.headers["WWW-Authenticate"] = 'basic realm="API login"'
end
end
end
if @status == 404
@summary ||= "Not found"
Expand Down
4 changes: 4 additions & 0 deletions src/api/config/options.yml.example
Expand Up @@ -35,6 +35,10 @@ frontend_protocol: https
#external_frontend_port: 443
#external_frontend_protocol: https

# Kerberos authentication
#kerberos_keytab: "/etc/krb5.keytab"
#kerberos_service_principal: "HTTP/hostname.example.com@EXAMPLE.COM"
#kerberos_realm: "EXAMPLE.COM"

extended_backend_log: false

Expand Down
93 changes: 79 additions & 14 deletions src/api/lib/authenticator.rb
@@ -1,3 +1,7 @@
if CONFIG['kerberos_service_principal']
require_dependency 'gssapi'
end

class Authenticator
class AuthenticationRequiredError < APIException
setup 401, "Authentication required"
Expand Down Expand Up @@ -25,7 +29,8 @@ class AdminUserRequiredError < APIException

attr_reader :request, :session, :user_permissions, :http_user

def initialize(request, session)
def initialize(request, session, response)
@response = response
@request = request
@session = session
@http_user = nil
Expand All @@ -40,9 +45,8 @@ def extract_user
if proxy_mode?
extract_proxy_user
else
extract_basic_auth_user

@http_user = User.find_with_credentials @login, @passwd if @login
extract_auth_user
@http_user = User.find_with_credentials @login, @passwd if @login && @passwd
end

if !@http_user && session[:login]
Expand Down Expand Up @@ -80,6 +84,65 @@ def require_admin

private

def initialize_krb_session
principal = CONFIG['kerberos_service_principal']

unless CONFIG['kerberos_realm']
CONFIG['kerberos_realm'] = principal.rpartition("@")[2]
end

krb = GSSAPI::Simple.new(
principal.partition("/")[2].rpartition("@")[0],
principal.partition("/")[0],
CONFIG['kerberos_keytab'] || "/etc/krb5.keytab"
)
krb.acquire_credentials

return krb
end

def extract_krb_user(authorization)
unless authorization[1]
Rails.logger.debug "Didn't receive any negotiation data."
@response.headers["WWW-Authenticate"] = authorization.join(' ')
raise AuthenticationRequiredError.new "GSSAPI negotiation failed."
end

begin
krb = initialize_krb_session

begin
tok = krb.accept_context(Base64.strict_decode64(authorization[1]))
rescue GSSAPI::GssApiError
@response.headers["WWW-Authenticate"] = authorization.join(' ')
raise AuthenticationRequiredError.new "Received invalid GSSAPI context."
end

unless krb.display_name.match("@#{CONFIG['kerberos_realm']}$")
@response.headers["WWW-Authenticate"] = authorization.join(' ')
raise AuthenticationRequiredError.new "User authenticated in wrong Kerberos realm."
end

unless tok == true
tok = Base64.strict_encode64(tok)
@response.headers["WWW-Authenticate"] = "Negotiate #{tok}"
end

@login = krb.display_name.partition("@")[0]
@http_user = User.find_by_login @login
raise AuthenticationRequiredError.new "User '#{@login}' has no account on the server." unless @http_user
rescue GSSAPI::GssApiError => err
raise AuthenticationRequiredError.new, "Received a GSSAPI exception; #{err.message}."
end
end

def extract_basic_user(authorization)
@login, @passwd = Base64.decode64(authorization[1]).split(':', 2)[0..1]

# set password to the empty string in case no password is transmitted in the auth string
@passwd ||= ""
end

def extract_proxy_user
proxy_user = request.env['HTTP_X_USERNAME']
if proxy_user
Expand Down Expand Up @@ -115,19 +178,21 @@ def extract_proxy_user
end
end

def extract_basic_auth_user
def extract_auth_user
authorization = authorization_infos

# privacy! Rails.logger.debug( "AUTH: #{authorization.inspect}" )

if authorization && authorization[0] == "Basic"
# Rails.logger.debug( "AUTH2: #{authorization}" )
@login, @passwd = Base64.decode64(authorization[1]).split(':', 2)[0..1]

# set password to the empty string in case no password is transmitted in the auth string
@passwd ||= ""
# privacy! logger.debug( "AUTH: #{authorization.inspect}" )
if authorization
# logger.debug( "AUTH2: #{authorization}" )
if authorization[0] == "Basic"
extract_basic_user authorization
elsif authorization[0] == "Negotiate" && CONFIG['kerberos_service_principal']
extract_krb_user authorization
else
Rails.logger.debug "Unsupported authentication string '#{authorization[0]}' received."
end
else
Rails.logger.debug "no authentication string was sent"
Rails.logger.debug "No authentication string was received."
end
end

Expand Down

0 comments on commit 8f8c507

Please sign in to comment.