Skip to content

Commit

Permalink
Move API OpenID-Connect/OAuth2 support from API to ManageIQ authentic…
Browse files Browse the repository at this point in the history
…ator.

Fixes #19865
  • Loading branch information
jvlcek committed Mar 6, 2020
1 parent 468521a commit e8f495c
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 3 deletions.
20 changes: 17 additions & 3 deletions app/models/authenticator/httpd.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
require_dependency 'authenticator/httpd_oauth2'

module Authenticator
class Httpd < Base
include HttpdOauth2

def self.proper_name
'External httpd'
end
Expand All @@ -25,9 +29,15 @@ def user_authorizable_without_authentication?
true
end

def _authenticate(_username, _password, request)
request.present? &&
request.headers['X-REMOTE-USER'].present?
def _authenticate(username, password, request)
if !user_data_collected?(request) && request.present? && oidc_configured?
if password.present?
oidc_authenticate(username, password, request)
else
authenticate_with_jwt(request)
end
end
user_data_collected?(request)
end

def failure_reason(_username, request)
Expand Down Expand Up @@ -157,5 +167,9 @@ def user_attrs_from_external_directory_via_dbus_api_service(username)

HttpdDBusApi.new.user_attrs(username, ATTRS_NEEDED)
end

def user_data_collected?(request)
request.present? && request.headers['X-REMOTE-USER'].present?
end
end
end
162 changes: 162 additions & 0 deletions app/models/authenticator/httpd_oauth2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
module HttpdOauth2
class HttpdOauth2Error < StandardError; end

require "net/http"
require "uri"

REMOTE_HEADERS =
{
"X-REMOTE-USER" => "preferred_username",
"X-REMOTE-USER-EMAIL" => "email",
"X-REMOTE-USER-FIRSTNAME" => "given_name",
"X-REMOTE-USER-LASTNAME" => "family_name",
"X-REMOTE-USER-FULLNAME" => "name",
"X-REMOTE-USER-GROUPS" => "groups",
"X-REMOTE-USER-DOMAIN" => "domain"
}.freeze

HTTPD_OPENIDC_CONF = Pathname.new("/etc/httpd/conf.d/manageiq-external-auth-openidc.conf").freeze

def oidc_configured?
config[:mode] == "httpd" && config[:provider_type] == "oidc" && config[:oidc_enabled] == true
end

def authenticate_with_jwt(request)
jwt_token = get_jwt_token_from_headers(request)
token_info = introspect_jwt_token(jwt_token)
define_jwt_request_headers(token_info, request)
rescue => e
raise HttpdOauth2Error, "Failed to Authenticate with JWT - error #{e}"
end

def oidc_authenticate(username, password, request)
user_jwt = get_jwt_token_from_oidc(username, password)
token_info = introspect_jwt_token(user_jwt)
define_jwt_request_headers(token_info, request)
end

def httpd_oidc_config
@httpd_oidc_config ||= HTTPD_OPENIDC_CONF.readlines.collect(&:chomp)
end

def httpd_oidc_config_param(name)
param_spec = httpd_oidc_config.find { |line| line =~ /^#{name} .*/i }
return "" if param_spec.blank?

param_match = param_spec.match(/^#{name} (.*)/i)
param_match ? param_match[1].strip : ""
end

def oidc_provider_metadata
@oidc_provider_metadata ||= begin
oidc_provider_metadata_url = httpd_oidc_config_param("OIDCProviderMetadataURL")
if oidc_provider_metadata_url.blank?
{}
else
uri = URI.parse(oidc_provider_metadata_url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == "https")
response = http.request(Net::HTTP::Get.new(uri.request_uri))
JSON.parse(response.body)
end
end
end

def oidc_metadata_url_endpoint(oidc_param, metadata_url_key)
endpoint = httpd_oidc_config_param(oidc_param)
endpoint = oidc_provider_metadata[metadata_url_key] if endpoint.blank?
raise HttpdOauth2Error, "Invalid #{HTTPD_OPENIDC_CONF} configuration, missing #{oidc_param} or OIDCProviderMetadataURL #{metadata_url_key} entry" if endpoint.blank?

endpoint
end

def oidc_token_endpoint
@oidc_token_endpoint ||= oidc_metadata_url_endpoint("OIDCProviderTokenEndpoint", "token_endpoint")
end

def oidc_token_introspection_endpoint
@oidc_token_introspection_endpoint ||= oidc_metadata_url_endpoint("OIDCOAuthIntrospectionEndpoint", "token_introspection_endpoint")
end

def oidc_client_id
@oidc_client_id ||= httpd_oidc_config_param("OIDCClientId")
end

def oidc_client_secret
@oidc_client_secret ||= httpd_oidc_config_param("OIDCClientSecret")
end

def oidc_scope
@oidc_scope ||= httpd_oidc_config_param("OIDCScope")
end

def get_jwt_token_from_headers(request)
jwt_token_match = request.headers["HTTP_AUTHORIZATION"].match(/^Bearer (.*)/)
jwt_token_match[1] if jwt_token_match
end

def get_jwt_token_from_oidc(username, password)
uri = URI.parse(oidc_token_endpoint)
request_params = {
"grant_type" => "password",
"username" => username,
"password" => password
}
request_params["scope"] = oidc_scope if oidc_scope.present?

post_request = Net::HTTP::Post.new(uri)
post_request.basic_auth(oidc_client_id, oidc_client_secret)
post_request.form_data = request_params

http_params = {:use_ssl => (uri.scheme == "https")}
response = Net::HTTP.start(uri.hostname, uri.port, http_params) { |http| http.request(post_request) }
parsed_response = JSON.parse(response.body)
raise HttpdOauth2Error, parsed_response["error_description"] if parsed_response["error"].present?

parsed_response["access_token"]
rescue => e
raise HttpdOauth2Error, "Failed to get a JWT Token for user #{username} - error #{e}"
end

def introspect_jwt_token(jwt_token)
uri = URI.parse(oidc_token_introspection_endpoint)
request_params = {
"token" => jwt_token
}
request_params["scope"] = oidc_scope if oidc_scope.present?

post_request = Net::HTTP::Post.new(uri)
post_request.basic_auth(oidc_client_id, oidc_client_secret)
post_request.form_data = request_params

http_params = {:use_ssl => (uri.scheme == "https")}
response = Net::HTTP.start(uri.hostname, uri.port, http_params) { |http| http.request(post_request) }
parsed_response = JSON.parse(response.body)

raise HttpdOauth2Error, "Invalid access token, JWT is inactive" if parsed_response["active"] != true

parsed_response
rescue => e
raise HttpdOauth2Error, "Failed to Validate the JWT - error #{e}"
end

def define_jwt_request_headers(token_info, request)
REMOTE_HEADERS.each do |rh, val|
request.headers[rh] = token_info[val]
end

if request.headers["X-REMOTE-USER-GROUPS"].present?
request.headers["X-REMOTE-USER-GROUPS"] = request.headers["X-REMOTE-USER-GROUPS"].join(",")
end

debug_headers(request)
end

def debug_headers(request)
return unless $log.debug?

REMOTE_HEADERS.keys.each do |rh|
$audit_log.info("request.headers[#{rh}] ->#{request.headers[rh]}<-")
end
end
end

0 comments on commit e8f495c

Please sign in to comment.