Skip to content

Commit

Permalink
Adding supporting for Azure AD access token (#837)
Browse files Browse the repository at this point in the history
* Adding supporting for Azure AD access token

* Added more comments for the AD access token skipif files

* Save the pointer to access token struct until after connecting

* Clear the access token data before freeing the memory

* Added a reference as per review
  • Loading branch information
yitam authored Sep 6, 2018
1 parent ae1b413 commit e513806
Show file tree
Hide file tree
Showing 15 changed files with 536 additions and 29 deletions.
10 changes: 10 additions & 0 deletions source/pdo_sqlsrv/pdo_dbh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ namespace PDOConnOptionNames {

const char Server[] = "Server";
const char APP[] = "APP";
const char AccessToken[] = "AccessToken";
const char ApplicationIntent[] = "ApplicationIntent";
const char AttachDBFileName[] = "AttachDbFileName";
const char Authentication[] = "Authentication";
Expand Down Expand Up @@ -185,6 +186,15 @@ const connection_option PDO_CONN_OPTS[] = {
CONN_ATTR_STRING,
conn_str_append_func::func
},
{
PDOConnOptionNames::AccessToken,
sizeof( PDOConnOptionNames::AccessToken ),
SQLSRV_CONN_OPTION_ACCESS_TOKEN,
ODBCConnOptions::AccessToken,
sizeof( ODBCConnOptions::AccessToken),
CONN_ATTR_STRING,
access_token_set_func::func
},
{
PDOConnOptionNames::ApplicationIntent,
sizeof( PDOConnOptionNames::ApplicationIntent ),
Expand Down
9 changes: 9 additions & 0 deletions source/pdo_sqlsrv/pdo_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,15 @@ pdo_error PDO_ERRORS[] = {
SQLSRV_ERROR_KEYSTORE_INVALID_VALUE,
{ IMSSP, (SQLCHAR*) "Invalid value for loading Azure Key Vault.", -89, false}
},
{
SQLSRV_ERROR_INVALID_OPTION_WITH_ACCESS_TOKEN,
{ IMSSP, (SQLCHAR*) "When using Azure AD Access Token, the connection string must not contain UID, PWD, or Authentication keywords.", -90, false}
},
{
SQLSRV_ERROR_EMPTY_ACCESS_TOKEN,
{ IMSSP, (SQLCHAR*) "The Azure AD Access Token is empty. Expected a byte string.", -91, false}
},

{ UINT_MAX, {} }
};

Expand Down
115 changes: 96 additions & 19 deletions source/shared/core_conn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,12 @@ sqlsrv_conn* core_sqlsrv_connect( _In_ sqlsrv_context& henv_cp, _In_ sqlsrv_cont
} // else driver_version not unknown
#endif // !_WIN32

// time to free the access token, if not null
if (conn->azure_ad_access_token != NULL) {
memset(conn->azure_ad_access_token->data, 0, conn->azure_ad_access_token->dataSize); // clear the memory
conn->azure_ad_access_token.reset();
}

CHECK_SQL_ERROR( r, conn ) {
throw core::CoreException();
}
Expand Down Expand Up @@ -759,35 +765,53 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou
{
bool mars_mentioned = false;
connection_option const* conn_opt;
bool access_token_used = false;

try {
// First of all, check if access token is specified. If so, check if UID, PWD, Authentication exist
// No need to check the keyword Trusted_Connection because it is not among the acceptable options for SQLSRV drivers
if (zend_hash_index_exists(options, SQLSRV_CONN_OPTION_ACCESS_TOKEN)) {
bool invalidOptions = false;

// UID and PWD have to be NULLs... throw an exception as long as the user has specified any of them in the connection string,
// even if they may be empty strings. Likewise if the keyword Authentication exists
if (uid != NULL || pwd != NULL || zend_hash_index_exists(options, SQLSRV_CONN_OPTION_AUTHENTICATION)) {
invalidOptions = true;
}

// Add the server name
common_conn_str_append_func( ODBCConnOptions::SERVER, server, strnlen_s( server ), connection_string TSRMLS_CC );

// if uid is not present then we use trusted connection.
if(uid == NULL || strnlen_s( uid ) == 0 ) {

connection_string += "Trusted_Connection={Yes};";
}
else {

bool escaped = core_is_conn_opt_value_escaped( uid, strnlen_s( uid ));
CHECK_CUSTOM_ERROR( !escaped, conn, SQLSRV_ERROR_UID_PWD_BRACES_NOT_ESCAPED ) {
CHECK_CUSTOM_ERROR(invalidOptions, conn, SQLSRV_ERROR_INVALID_OPTION_WITH_ACCESS_TOKEN ) {
throw core::CoreException();
}

common_conn_str_append_func( ODBCConnOptions::UID, uid, strnlen_s( uid ), connection_string TSRMLS_CC );
access_token_used = true;
}

// if no password was given, then don't add a password to the connection string. Perhaps the UID
// given doesn't have a password?
if( pwd != NULL ) {
escaped = core_is_conn_opt_value_escaped( pwd, strnlen_s( pwd ));
CHECK_CUSTOM_ERROR( !escaped, conn, SQLSRV_ERROR_UID_PWD_BRACES_NOT_ESCAPED ) {
// Add the server name
common_conn_str_append_func( ODBCConnOptions::SERVER, server, strnlen_s( server ), connection_string TSRMLS_CC );

// if uid is not present then we use trusted connection -- but not when access token is used, because they are incompatible
if (!access_token_used) {
if (uid == NULL || strnlen_s(uid) == 0) {
connection_string += CONNECTION_OPTION_NO_CREDENTIALS; // "Trusted_Connection={Yes};"
}
else {
bool escaped = core_is_conn_opt_value_escaped(uid, strnlen_s(uid));
CHECK_CUSTOM_ERROR(!escaped, conn, SQLSRV_ERROR_UID_PWD_BRACES_NOT_ESCAPED) {
throw core::CoreException();
}

common_conn_str_append_func( ODBCConnOptions::PWD, pwd, strnlen_s( pwd ), connection_string TSRMLS_CC );
common_conn_str_append_func(ODBCConnOptions::UID, uid, strnlen_s(uid), connection_string TSRMLS_CC);

// if no password was given, then don't add a password to the connection string. Perhaps the UID
// given doesn't have a password?
if (pwd != NULL) {
escaped = core_is_conn_opt_value_escaped(pwd, strnlen_s(pwd));
CHECK_CUSTOM_ERROR(!escaped, conn, SQLSRV_ERROR_UID_PWD_BRACES_NOT_ESCAPED) {
throw core::CoreException();
}

common_conn_str_append_func(ODBCConnOptions::PWD, pwd, strnlen_s(pwd), connection_string TSRMLS_CC);
}
}
}

Expand Down Expand Up @@ -1172,3 +1196,56 @@ size_t core_str_zval_is_true( _Inout_ zval* value_z )

return 0; // false
}

void access_token_set_func::func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC )
{
SQLSRV_ASSERT(Z_TYPE_P(value) == IS_STRING, "An access token must be a byte string.");

size_t value_len = Z_STRLEN_P(value);

CHECK_CUSTOM_ERROR(value_len <= 0, conn, SQLSRV_ERROR_EMPTY_ACCESS_TOKEN) {
throw core::CoreException();
}

const char* value_str = Z_STRVAL_P( value );

// The SQL_COPT_SS_ACCESS_TOKEN pre-connection attribute allows the use of an access token (in the format extracted from
// an OAuth JSON response), obtained from Azure AD for authentication instead of username and password, and also
// bypasses the negotiation and obtaining of an access token by the driver. To use an access token, set the
// SQL_COPT_SS_ACCESS_TOKEN connection attribute to a pointer to an ACCESSTOKEN structure
//
// typedef struct AccessToken
// {
// unsigned int dataSize;
// char data[];
// } ACCESSTOKEN;
//
// NOTE: The ODBC Driver version 13.1 only supports this authentication on Windows.
//
// A valid access token byte string must be expanded so that each byte is followed by a 0 padding byte,
// similar to a UCS-2 string containing only ASCII characters
//
// See https://docs.microsoft.com/sql/connect/odbc/using-azure-active-directory#authenticating-with-an-access-token

size_t dataSize = 2 * value_len;

sqlsrv_malloc_auto_ptr<ACCESSTOKEN> accToken;
accToken = reinterpret_cast<ACCESSTOKEN*>(sqlsrv_malloc(sizeof(ACCESSTOKEN) + dataSize));

ACCESSTOKEN *pAccToken = accToken.get();
SQLSRV_ASSERT(pAccToken != NULL, "Something went wrong when trying to allocate memory for the access token.");

pAccToken->dataSize = dataSize;

// Expand access token with padding bytes
for (size_t i = 0, j = 0; i < dataSize; i += 2, j++) {
pAccToken->data[i] = value_str[j];
pAccToken->data[i+1] = 0;
}

core::SQLSetConnectAttr(conn, SQL_COPT_SS_ACCESS_TOKEN, reinterpret_cast<SQLPOINTER>(pAccToken), SQL_IS_POINTER);

// Save the pointer because SQLDriverConnect() will use it to make connection to the server
conn->azure_ad_access_token = pAccToken;
accToken.transferred();
}
14 changes: 10 additions & 4 deletions source/shared/core_sqlsrv.h
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,8 @@ struct sqlsrv_conn : public sqlsrv_context {
col_encryption_option ce_option; // holds the details of what are required to enable column encryption
DRIVER_VERSION driver_version; // version of ODBC driver

sqlsrv_malloc_auto_ptr<ACCESSTOKEN> azure_ad_access_token;

// initialize with default values
sqlsrv_conn( _In_ SQLHANDLE h, _In_ error_callback e, _In_opt_ void* drv, _In_ SQLSRV_ENCODING encoding TSRMLS_DC ) :
sqlsrv_context( h, SQL_HANDLE_DBC, e, drv, encoding )
Expand Down Expand Up @@ -1105,6 +1107,7 @@ enum SQLSRV_STMT_OPTIONS {
namespace ODBCConnOptions {

const char APP[] = "APP";
const char AccessToken[] = "AccessToken";
const char ApplicationIntent[] = "ApplicationIntent";
const char AttachDBFileName[] = "AttachDbFileName";
const char Authentication[] = "Authentication";
Expand Down Expand Up @@ -1140,6 +1143,7 @@ enum SQLSRV_CONN_OPTIONS {

SQLSRV_CONN_OPTION_INVALID,
SQLSRV_CONN_OPTION_APP,
SQLSRV_CONN_OPTION_ACCESS_TOKEN,
SQLSRV_CONN_OPTION_CHARACTERSET,
SQLSRV_CONN_OPTION_CONN_POOLING,
SQLSRV_CONN_OPTION_DATABASE,
Expand Down Expand Up @@ -1222,14 +1226,14 @@ struct driver_set_func {
static void func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC );
};

struct ce_ksp_provider_set_func {
static void func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC );
};

struct ce_akv_str_set_func {
static void func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC );
};

struct access_token_set_func {
static void func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC );
};


// factory to create a connection (since they are subclassed to instantiate statements)
typedef sqlsrv_conn* (*driver_conn_factory)( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* drv TSRMLS_DC );
Expand Down Expand Up @@ -1718,6 +1722,8 @@ enum SQLSRV_ERROR_CODES {
SQLSRV_ERROR_AKV_SECRET_MISSING,
SQLSRV_ERROR_KEYSTORE_INVALID_VALUE,
SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED,
SQLSRV_ERROR_INVALID_OPTION_WITH_ACCESS_TOKEN,
SQLSRV_ERROR_EMPTY_ACCESS_TOKEN,

// Driver specific error codes starts from here.
SQLSRV_ERROR_DRIVER_SPECIFIC = 1000,
Expand Down
7 changes: 7 additions & 0 deletions source/shared/msodbcsql.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
#define SQL_COPT_SS_TRUSTEDCMKPATHS (SQL_COPT_SS_BASE_EX+13)// List of trusted CMK paths
#define SQL_COPT_SS_CEKCACHETTL (SQL_COPT_SS_BASE_EX+14)// Symmetric Key Cache TTL
#define SQL_COPT_SS_AUTHENTICATION (SQL_COPT_SS_BASE_EX+15)// The authentication method used for the connection
#define SQL_COPT_SS_ACCESS_TOKEN (SQL_COPT_SS_BASE_EX+16)// The authentication access token used for the connection

// SQLColAttributes driver specific defines.
// SQLSetDescField/SQLGetDescField driver specific defines.
Expand Down Expand Up @@ -370,6 +371,12 @@
#pragma warning(disable:4200)
#endif

typedef struct AccessToken
{
unsigned int dataSize;
char data[];
} ACCESSTOKEN;

// Keystore Provider interface definition
typedef struct CEKeystoreContext
{
Expand Down
10 changes: 10 additions & 0 deletions source/sqlsrv/conn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ namespace SSConnOptionNames {
// most of these strings are the same for both the sqlsrv_connect connection option
// and the name put into the connection string. MARS is the only one that's different.
const char APP[] = "APP";
const char AccessToken[] = "AccessToken";
const char ApplicationIntent[] = "ApplicationIntent";
const char AttachDBFileName[] = "AttachDbFileName";
const char Authentication[] = "Authentication";
Expand Down Expand Up @@ -257,6 +258,15 @@ const connection_option SS_CONN_OPTS[] = {
CONN_ATTR_STRING,
conn_str_append_func::func
},
{
SSConnOptionNames::AccessToken,
sizeof( SSConnOptionNames::AccessToken ),
SQLSRV_CONN_OPTION_ACCESS_TOKEN,
ODBCConnOptions::AccessToken,
sizeof( ODBCConnOptions::AccessToken),
CONN_ATTR_STRING,
access_token_set_func::func
},
{
SSConnOptionNames::ApplicationIntent,
sizeof( SSConnOptionNames::ApplicationIntent ),
Expand Down
8 changes: 8 additions & 0 deletions source/sqlsrv/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,14 @@ ss_error SS_ERRORS[] = {
SQLSRV_ERROR_KEYSTORE_INVALID_VALUE,
{ IMSSP, (SQLCHAR*) "Invalid value for loading Azure Key Vault.", -114, false}
},
{
SQLSRV_ERROR_INVALID_OPTION_WITH_ACCESS_TOKEN,
{ IMSSP, (SQLCHAR*) "When using Azure AD Access Token, the connection string must not contain UID, PWD, or Authentication keywords.", -115, false}
},
{
SQLSRV_ERROR_EMPTY_ACCESS_TOKEN,
{ IMSSP, (SQLCHAR*) "The Azure AD Access Token is empty. Expected a byte string.", -116, false}
},

// terminate the list of errors/warnings
{ UINT_MAX, {} }
Expand Down
3 changes: 3 additions & 0 deletions test/functional/pdo_sqlsrv/access_token.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php
$accToken = 'TARGET_ACCESS_TOKEN';
?>
Loading

0 comments on commit e513806

Please sign in to comment.