Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for OpenID-Connect/OAuth2 in the API #737

Merged
merged 2 commits into from
Feb 20, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 136 additions & 3 deletions app/controllers/api/base_controller/authentication.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
module Api
class BaseController
module Authentication
require "net/http"
require "uri"

SYSTEM_TOKEN_TTL = 30.seconds

def auth_mechanism
Expand All @@ -9,8 +12,12 @@ def auth_mechanism
elsif request.headers[HttpHeaders::AUTH_TOKEN]
:token
elsif request.headers["HTTP_AUTHORIZATION"]
# For AJAX requests the basic auth type should be distinguished
request.headers['X-REQUESTED-WITH'] == 'XMLHttpRequest' ? :basic_async : :basic
if jwt_token
:jwt
else
# For AJAX requests the basic auth type should be distinguished
request.headers['X-REQUESTED-WITH'] == 'XMLHttpRequest' ? :basic_async : :basic
end
elsif request.x_csrf_token
# Even if the session cookie is not set, we want to consider a request
# as a UI authentication request. Otherwise the response would force
Expand All @@ -34,10 +41,22 @@ def require_api_user_or_token
when :ui_session
raise AuthenticationError unless valid_ui_session?
auth_user(session[:userid])
when :jwt
authenticate_with_jwt(jwt_token)
when :basic, :basic_async, nil
success = authenticate_with_http_basic do |u, p|
begin
timeout = ::Settings.api.authentication_timeout.to_i_with_method

if oidc_configuration?
# Basic auth, user/password but configured against OpenIDC.
# Let's authenticate as such and get a JWT for that user.
#
user_jwt = get_jwt_token(u, p)
token_info = validate_jwt_token(user_jwt)
user_data, membership = user_details_from_jwt(token_info)
define_jwt_request_headers(user_data, membership)
end
user = User.authenticate(u, p, request, :require_user => true, :timeout => timeout)
auth_user(user.userid)
rescue MiqException::MiqEVMLoginError => e
Expand All @@ -51,7 +70,7 @@ def require_api_user_or_token
api_log_error("AuthenticationError: #{e.message}")
response.headers["Content-Type"] = "application/json"
case auth_mechanism
when :system, :token, :ui_session, :basic_async
when :jwt, :system, :token, :ui_session, :basic_async
render :status => 401, :json => ErrorSerializer.new(:unauthorized, e).serialize(true).to_json
when :basic, nil
request_http_basic_authentication("Application", ErrorSerializer.new(:unauthorized, e).serialize(true).to_json)
Expand Down Expand Up @@ -96,6 +115,18 @@ def auth_user(userid)
User.current_user = auth_user_obj
end

def authenticate_with_jwt(jwt_token)
token_info = validate_jwt_token(jwt_token)
user_data, membership = user_details_from_jwt(token_info)
define_jwt_request_headers(user_data, membership)

timeout = ::Settings.api.authentication_timeout.to_i_with_method
user = User.authenticate(user_data[:username], "", request, :require_user => true, :timeout => timeout)
auth_user(user.userid)
rescue => e
raise AuthenticationError, "Failed to Authenticate with JWT - error #{e}"
end

def authenticate_with_user_token(auth_token)
if !api_token_mgr.token_valid?(auth_token)
raise AuthenticationError, "Invalid Authentication Token #{auth_token} specified"
Expand Down Expand Up @@ -142,6 +173,108 @@ def valid_ui_session?
request.origin.nil? || request.origin == request.base_url # origin header if set matches base_url
].all?
end

# Support for OAuth2 Authentication
#
# Some of this stuff should probably live in manageiq/app/models/authenticator/httpd.rb
#
HTTPD_OPENIDC_CONF = Pathname.new("/etc/httpd/conf.d/manageiq-external-auth-openidc.conf")

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

def oidc_configuration?
auth_config = Settings.authentication
auth_config.mode == "httpd" &&
auth_config.oidc_enabled &&
auth_config.provider_type == "oidc" &&
HTTPD_OPENIDC_CONF.exist?
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 }
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")
uri = URI.parse(oidc_provider_metadata_url)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(Net::HTTP::Get.new(uri.request_uri))
JSON.parse(response.body)
end
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 get_jwt_token(username, password)
uri = URI.parse(oidc_provider_metadata["token_endpoint"])
response = Net::HTTP.post_form(uri, "grant_type" => "password",
"client_id" => oidc_client_id,
"client_secret" => oidc_client_secret,
"username" => username,
"password" => password)

parsed_response = JSON.parse(response.body)
raise parsed_response["error_description"] if parsed_response["error"].present?

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

def validate_jwt_token(jwt_token)
uri = URI.parse(oidc_provider_metadata["token_introspection_endpoint"])
response = Net::HTTP.post_form(uri, "client_id" => oidc_client_id,
"client_secret" => oidc_client_secret,
"token" => jwt_token)

parsed_response = JSON.parse(response.body)
raise "Invalid access token, JWT is inactive" if parsed_response["active"] != true

# Return the Token Introspection result
parsed_response
rescue => e
raise AuthenticationError, "Failed to Validate the JWT - error #{e}"
end

def user_details_from_jwt(token_info)
user_attrs = {
:username => token_info["username"],
:fullname => token_info["name"],
:firstname => token_info["given_name"],
:lastname => token_info["family_name"],
:email => token_info["email"],
:domain => token_info["domain"]
}
[user_attrs, Array(token_info["groups"])]
end

def define_jwt_request_headers(user_data, membership)
request.headers["X-REMOTE-USER"] = user_data[:username]
request.headers["X-REMOTE-USER-FULLNAME"] = user_data[:fullname]
request.headers["X-REMOTE-USER-FIRSTNAME"] = user_data[:firstname]
request.headers["X-REMOTE-USER-LASTNAME"] = user_data[:lastname]
request.headers["X-REMOTE-USER-EMAIL"] = user_data[:email]
request.headers["X-REMOTE-USER-DOMAIN"] = user_data[:domain]
request.headers["X-REMOTE-USER-GROUPS"] = membership.join(',')
end
end
end
end