Using an External Credentials Service
In addition to built-in support for AD FS, Azure AD, and Okta, the Windows version of the Simba Athena ODBC Driver also provides support for other credentials services. The connector can authenticate connections using any SAML-based credential provider plugin of your choice.
To configure an external credentials service:
- Create an IAM profile that specifies the credential provider plugin and other authentication parameters as needed. The profile must be ASCII-encoded, and must contain the following key-value pair, where [PluginPath] is the full path to the plugin application:
- Configure the connector to use this profile.
plugin_name = [PluginPath]
For example:
plugin_name = C:\Users\jsmith\ApplicationInstallDir\CredServiceApplication.exe
For information about how to create a profile, see "Using a Configuration Profile" in the Amazon Redshift Cluster Management Guide: https://docs.aws.amazon.com/redshift/latest/mgmt/options-for-providing-iam-credentials.html#using-configuration-profile.
The connector detects and uses the authentication settings specified in the profile.
The following code sample shows how to create the .exe
file for an external credential service:
#include "SampleIAMWinHttpClient.h"
#include <aws/core/Aws.h>
#include <aws/core/http/HttpTypes.h>
#include <aws/core/utils/json/JsonSerializer.h>
#include <map>
#include <regex>
#include <string>
#include <Windows.h>
#include <winhttp.h>
/* Json::JsonValue class contains a member function: GetObject. There is a predefined
* MACRO GetObject in wingdi.h that will cause the conflict. We need to undef GetObject
* in order to use the GetObject memeber function from Json::JsonValue
*/
#ifdef GetObject
#undef GetObject
#endif
using namespace std;
using namespace Aws::Client;
using namespace Aws::Http;
using namespace Aws::Utils;
namespace
{
static const string IAM_KEY_IDP_HOST = "idp_host"
static const string IAM_KEY_APP_ID = "app_id";
static const string IAM_KEY_APP_NAME = "app_name";
static const string IAM_KEY_USER = "user";
static const string IAM_KEY_PASSWORD = "password";
static const string OKTA_SESSION_TOKEN_STATUS = "status";
static const string OKTA_SESSION_TOKEN_STATUS_SUCCESS = "SUCCESS";
static const string OKTA_SESSION_TOKEN = "sessionToken";
}
/*//////////////////////////////////////////////
// @brief Replace all instances of findString with
// replaceWith.
//
// @param source The original string and return as a
// modified string.
// @param oldValue The value to be replaced.
// @param newValue The value to use as a replacement.
/////////////////////////////////////////////*/
void FindAndReplace(string& source, string& oldValue, string& newValue)
{
size_t pos = source.find(oldValue);
while (pos != string::npos)
{
source.replace(pos, oldValue.length(), newValue);
pos = source.find(oldValue, (pos + newValue.length()));
}
return;
}
/*//////////////////////////////////////////////
// @brief Get the list of connection and
// authentication parameters (key-value pairs) from
// the AWS profile. The connector will convert them into
// [key]=[value] arguments and pass to this program.
// Sample command line:
// IAMExternalPlugin.exe idp_
// host=sampleidphost.okta.com app_id=sampleappid app_
// name=amazon_aws
// user=sampleuser password=samplepassword
//
// @param argc The number of command-line arguments.
// @param argv The argument vector.
//
// @return A list of key-value pairs used to connect
// and authenticate an Okta server.
////////////////////////////////////////////*/
std::map<std::string, std::string> GetArgsList(int argc, char* argv[])
{
map<string, string> result;
const string DELIMITER = "=";
for (int i = 0; i < argc; ++i)
{
string arg(argv[i]);
size_t pos = arg.find(DELIMITER);
if (string::npos != pos)
{
string key = arg.substr(0, pos);
string value = arg.substr(pos + 1);
result[key] = value;
}
}
return result;
}
/*//////////////////////////////////////
// @brief Create a WinHttp client with AWS client
// configuration.
// SampleIAMWinHttpClient is implemented using the
// AWS SDK for C++.
// You can use any HTTP client of your choice.
//
// @param config The AWS client configuration.
//
// @return SampleIAMWinHttpClient.
//////////////////////////////////////*/
std::shared_ptr<SampleIAMWinHttpClient> GetHttpClient(const ClientConfiguration& config)
{
return Aws::MakeShared<SampleIAMWinHttpClient>("SampleIAMWinHttpClient", config);
}
/*///////////////////////////////////////////
// @brief Get the SAML response, in base64-encoded
// ASCII string, returned by the specific
// SAML provider being used for this implementation.
// How you get this string will depend on
// the specific SAML provider you are using. A sample
// implementation of OKTA credentials
// provider is given below.
//
// @param args The list of connection and
// authentication parameters.
//
// @return The SAML response string.
//////////////////////////////////////////*/
std::string GetSamlResponse(std::map<std::string, std::string> args)
{
string samlResponse;
// Initialize AWS SDK before creating service
// clients and using them
Aws::SDKOptions options;
Aws::InitAPI(options);
// The URL endpoint for OKTA authentication
string uri = "https://" + args[IAM_KEY_IDP_HOST] + "/api/v1/authn";
// HTTP Request header
const map<string, string> requestHeaders =
{
{ "Accept", "application/json" },
{ "Content-Type", "application/json; charset=utf-8" },
{ "Cache-Control", "no-cache" }
};
// OKTA authentication parameters
const map<string, string> requestParamMap =
{
{ "username", args[IAM_KEY_USER] },
{ "password", args[IAM_KEY_PASSWORD] }
};
// Create a HTTP request body from the
// authentication parameters
Json::JsonValue requestBodyJson;
for (const auto& param : requestParamMap)
{
requestBodyJson.WithString(param.first, param.second);
}
Json::JsonView requestBodyView(requestBodyJson);
const string requestBody = requestBodyView.WriteReadable();
// Create an IAMHttpClient with HttpClient
// configuration.
ClientConfiguration config;
std::shared_ptr<SampleIAMWinHttpClient> client = GetHttpClient(config);
// Send HTTP POST request to the OKTA server for
// authentication
std::shared_ptr<Aws::Http::HttpResponse> response = client->MakeHttpRequest(
uri,
HttpMethod::HTTP_POST,
requestHeaders,
requestBody);
// Check the response code from the HTTP POST
// request
if (HttpResponseCode::OK != response->GetResponseCode())
{
cerr << "Connection or authentication failed to the Okta server. Response code: ";
cerr << static_cast<int>(response->GetResponseCode()) << endl;
return samlResponse;
}
// Parse the response body to a JSON document
Aws::Utils::Json::JsonValue jsonValue(response->GetResponseBody());
if (!jsonValue.WasParseSuccessful())
{
cerr << "Failed to parse the response into JSON: ";
cerr << jsonValue.GetErrorMessage() << endl;
return samlResponse;
}
// Check whether the status of the session token
// request succeeded
string status;
string sessionToken;
Json::JsonView jsonNodeView(jsonValue);
if (jsonNodeView.ValueExists(OKTA_SESSION_TOKEN_STATUS) && jsonNodeView.GetObject(OKTA_SESSION_TOKEN_STATUS).IsString())
{
status = jsonNodeView.GetString(OKTA_SESSION_TOKEN_STATUS);
}
// Extract the session token from the response
if (OKTA_SESSION_TOKEN_STATUS_SUCCESS == status)
{
if (jsonNodeView.ValueExists(OKTA_SESSION_TOKEN) && jsonNodeView.GetObject(OKTA_SESSION_TOKEN).IsString())
{
sessionToken = jsonNodeView.GetString(OKTA_SESSION_TOKEN);
}
}
if (sessionToken.empty())
{
cerr << "Failed to retrieve Okta session token." << endl;
return samlResponse;
}
// Sending HTTP GET request using the session
// token to get the SAML response from server
uri = "https://" + args[IAM_KEY_IDP_HOST] + "/home/" + args[IAM_KEY_APP_NAME] + "/" + args[IAM_KEY_APP_ID] + "?onetimetoken=" + sessionToken;
std::shared_ptr<Aws::Http::HttpResponse> responseGet = client->MakeHttpRequest(uri, HttpMethod::HTTP_GET);
if (HttpResponseCode::OK != responseGet->GetResponseCode())
{
cerr << "Failed to retrieve SAML assertion from the Okta server. Response code: ";
cerr << static_cast<int>(response->GetResponseCode()) << endl;
return samlResponse;
}
// Extract the SAML response from the HTTP
// responseAws::StringStream reponseBody;
reponseBody << responseGet->GetResponseBody().rdbuf();
string resBody = reponseBody.str();
std::smatch match;
std::regex expression("SAMLResponse.+?value=\"([^\"]+)\"");
if (!std::regex_search(resBody, match, expression))
{
cerr << "SAML assertion not found." << endl;
return samlResponse;
}
samlResponse = match.str(1);
string findString = "+";
string replaceWith = "+";
FindAndReplace(samlResponse, findString, replaceWith);
findString = "=";
replaceWith = "=";
FindAndReplace(samlResponse, findString, replaceWith);
// Shutdown AWS SDK
Aws::ShutdownAPI(options);
// Return the SAML response
return samlResponse;
}
/*//////////////////////////////////////////////
// @breif The program starts here. The OKTA connection
// and authentication parameters are passed
// from the command line.
//
// @param argc The number of command-line arguments.
// @param argv The argument vector.
/////////////////////////////////////////////*/
void main(int argc, char* argv[])
{
// Get the list of connection and authentication
// parameters (key-value pairs) from the AWS
// profile.
std::map<std::string, std::string> args(GetArgsList(argc, argv));
// Get the SAML response in base64-encoded ASCII
// string.
// Example of SAML response:
// https://www.samltool.com/generic_sso_res.php// TODO: This is the method you need to implement in
// order to get the SAML response string
// returned by the specific SAML provider.
std::string samlResponse = GetSamlResponse(args);
// Send the SAML response string to the StdOutput.
// The Athena ODBC Connector then decodes and
// authenticates the Athena server using the SAML
// response.
std::cout << samlResponse;
}