-
Notifications
You must be signed in to change notification settings - Fork 562
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
accessToken support? #228
Comments
Unfortunately I don't think so without some code changes The ODBC connection string is passed as-is to the driver, so companies can support any keywords they want. Unfortunately MS didn't do the easy, obvious thing and allow an "accessToken=xyz" keyword in the connection string, at least as far as I can tell. This indicate you need to set it as a connection attribute before connecting. Unfortunately it also shows you need to pass a binary structure (thanks MS!) which pyodbc does not support yet. I could add this constant and recognize it as a pointer type, but it would probably be better to come up with Python data type that is known to be passed as a pointer (SQL_IS_POINTER). In Python 3, SQL_COPT_SS_ACCESS_TOKEN = 1256
token3 = b'...'
pyodbc.connect("Driver={..};Database=xyz", attrs_before={
SQL_COPT_SS_ACCESS_TOKEN: token
}) Encoding the token properly would require using the |
The access token is usually very long (>1KB, and several KB is not uncommon), can contain arbitrary data, and driver managers may have hardcoded limits on how long a connection string may be. Even pyodbc currently has hardcoded limit of 600 (why? A quick glance through the code doesn't reveal any fixed-length buffers.) This page describes how to use an access token. I believe |
Give this a try... https://github.com/v-chojas/pyodbc/tree/connattrs I implemented the following type mapping:
This works with AAD access tokens. Example code to expand the token and prepend the length as described on the page linked above, in Python 2.x:
3.x is only slightly more involved due to annoying char/bytes split:
(SQL_COPT_SS_ACCESS_TOKEN is 1256; it's specific to msodbcsql driver so pyodbc does not have it defined, and likely will not.) |
Hi! Something new about this issue? Could anyone merge the connattrs to master? |
also interested having this feature, any news? |
It's waiting for #259 to be merged. |
The #259 has been merged, this issue can be closed now. @adw555 @Pcosmin @hexadite-idan you can try this now in pyODBC 4.0.24. |
This particular reply from @v-chojas really helped me to succeed in getting token based access to the database.
Listed below you will find an alternative implementation for creating the As you can see, I changed the def str2mswin_bstr(value: str) -> bytes:
"""Convert a string to a (MS-Windows) BSTR.
See https://github.com/mkleehammer/pyodbc/issues/228#issuecomment-319190980
for the original code (although the input in that example are bytes,
assumed to contain characters in the range 0..255). It appears the string
is converted to an MS-Windows BSTR in 'Little-endian' format.
Please note that neither this routine (nor the original code) will produce
correct output if any code point in the input string is not in the
ISO/IEC 10646 Basic Multilingual Plane (BMP), assuming the resulting
structure is expected to hold UCS-2 characters.
See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp\
/692a42a9-06ce-4394-b9bc-5d2a50440168
for more info on BSTR. It does not state however what kind of encoding
it contains.
See https://en.wikipedia.org/wiki/Universal_Coded_Character_Set for info
on UCS-2.
:param value: the string to convert
:return: the converted value
"""
# The original code from the provided github URL applies 'Little-endian'
# to the character conversion in a python for-loop.
# The following is much faster and works safely for the whole ASCII range.
# BTW, there is no Byte Order Mark (BOM) inserted when specifying an
# explicit endianness.
encoded_bytes = value.encode("utf_16_le")
# The original code applies native endianness for encoding the length
# prefix. My working assumption that also the length should be encoded
# 'Little-endian', regardless of the CPU (and OS) this code is running on.
return struct.pack("<i", len(encoded_bytes)) + encoded_bytes In due time, I will provide this function as a small utility package in PyPI. |
There is no guarantee that values in an access token will be ASCII.
|
Ooh, that's interesting! First of all - for completeness' sake - I regrettably have to withdraw my statement that I got it working. I didn't check thoroughly enough. Still working on it... But regarding your remark about binary authentication data, if the token could be binary, how is it possible that I use the following code fragment to get the token that I'm passing as the credentials = MSIAuthentication(resource="https://database.windows.net/")
access_token = credentials.token["access_token"]
[But perhaps I'm using the wrong code to obtain the token...] |
The fact that an access-token-producing API is only producing ASCII tokens does not mean that consumers of tokens can assume that they are, given that this is a very generic authentication method. Or put another way, ASCII-only is just a subset of binary. Also, note that msodbcsql17 since 17.3 supports MSI authentication itself: https://docs.microsoft.com/en-us/sql/connect/odbc/using-azure-active-directory (Use |
In fact you are absolutely right about that. The producing API I am using (i.e. And this approach is working now (in a (Linux) Docker container)! Having said all this, the earlier listed conversion function can be replaced by the following one, which is about a factor of 13 faster, compared to the original code: import struct
from itertools import chain, repeat
def bytes2mswin_bstr(value: bytes) -> bytes:
"""Convert a sequence of bytes into a (MS-Windows) BSTR (as bytes).
See https://github.com/mkleehammer/pyodbc/issues/228#issuecomment-319190980
for the original code. It appears the input is converted to an
MS-Windows BSTR (in 'Little-endian' format).
See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp\
/692a42a9-06ce-4394-b9bc-5d2a50440168
for more info on BSTR.
:param value: the sequence of bytes to convert
:return: the converted value (as a sequence of bytes)
"""
# The original code from the provided github URL applies 'Little-endian'
# to the byte conversion in a python for-loop.
# The following is code is assumed to be faster.
encoded_bytes = bytes(chain.from_iterable(zip(value, repeat(0))))
# The original code applies native endianness for encoding the length
# prefix. My working assumption that also the length should be encoded
# 'Little-endian', regardless of the CPU (and OS) this code is running on.
return struct.pack("<i", len(encoded_bytes)) + encoded_bytes Do you agree that also the length should be encoded 'Little-endian', regardless of the CPU (and OS) this code is running on?
I have not tried this out, nor do I intend to (a bit frightened by the Note about MacOS and Linux support), now that I have it working. But thanks again for this solution and your support! :-) |
The only platforms that the ODBC Driver for SQL Server is available on are LE. |
I am having issues connecting via
But I get this error when trying to make
or, from a Windows machine:
|
In my case, the solution was explained here, but basically, my SQL admin had to add permissions for the app-registration to the specific database that I was trying to access. |
There is lot of communication happened in the thread, however, it is very difficult to come to conclusion based on the above errors, here you go a full working code: Python 2.x: Using sqlalchemy from sqlalchemy import create_engine Retrieve tokenheaders = {'Metadata': 'true'} response = requests.get('http://169..../metadata/identity/oauth2/token', headers=headers, params= params) token = response.json().get('access-token') CONNSTRING = "DRIVER={"+yourdriver+"};SERVER="+your_server+"DATABASE="+your_database+";Authentication=ActiveDirectoryMsi" CONNSTRING is main issue, and you need to carefully construct ityou can implement it using pyodbc as well, please message if you still have problems |
I have tried to do a variant of the preceding. First in a terminal, I log in to our Azure environment. Then I create a python script whose purpose is to access one of our Azure SQL db's and do some processing. Since we have multi-factor auth I need a token as well as user credentials. The python script begins by executing a CLI command to get a token.
The access token that gets returned is a <class 'bytes'> with a length of 1942. Since it's a 'bytes' class I didn't think I'd have to further encode it? That said, if I take the access token returned by 'az account get-access-token ...' and pass it into the following the process simply times out.
I seem to be doing what has been suggested except for the tokenstruct creation. Any thoughts or guidance would be appreciated. |
Yes, you need to pass the token in the correct format to the driver, which includes prepending a length field and expanding the bytes with 0-padding if necessary. |
@v-chojas Yeah, I tried this from @michaelcapizzi.:
It ends the same way ie. login timeout |
Have you got the accessToken from sql paas?
…On Wed, Mar 11, 2020 at 9:08 PM MikeB2019x ***@***.***> wrote:
@v-chojas <https://github.com/v-chojas> Yeah, I tried this from
@michaelcapizzi <https://github.com/michaelcapizzi>.:
tokenb = bytes(token, "UTF-8")
exptoken = b''
for i in tokenb:
exptoken += bytes({i})
exptoken += bytes(1)
tokenstruct = struct.pack("=i", len(exptoken)) + exptoken
tokenstruct
SQL_COPT_SS_ACCESS_TOKEN = 1256
conn = pyodbc.connect(url, attrs_before = { SQL_COPT_SS_ACCESS_TOKEN:tokenb })
cursor = conn.cursor()
It ends the same way ie. login timeout
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#228 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AA6H6WFBGV56TTY26AHLNO3RG74VTANCNFSM4DHRUILQ>
.
--
Thanks & Regards,
Sivagopal.B
|
Check that your access token structure is in the correct format, you can hexdump it to verify; e.g. if it was 1942 bytes before the expansion, and began with "eyJ0e...", then it should be 3884 after and the token structure should start with 2C 0F 00 00 65 00 79 00 4A 00 30 00 65 00 ... Also, do not specify UID, PWD, nor Authentication when using an access token - the access token is the credential to access the database, and the only one you need. |
@sivagopal I have the access token. I use the following to get it:
The length of the 'reponse' is 1942. I see that the 'response' is an object with an attribute 'accessToken' but also others such as 'tenant' and 'expiresOn'. I've assumed I just need the 'accessToken'. The length of 'token' is 1738 and it begins with 'eyJ0...' . @v-chojas after the expansion loop the length is 3480. So:
And after expansion:
Does that look right? In either case the login fails. If I don't include UID/PSSWD the error is as follows which looks like it doesn't recognize the user(?!):
This seems very difficult for what I expected to be straightforward. I greatly appreciate |
You don't need uid and password. If the sql paas is based on managed
identity, if it is a static password basis, then you need to pass
authentication bearer, I can provide you an example if you are not sure
…On Thu, 12 Mar 2020, 15:29 MikeB2019x, ***@***.***> wrote:
@sivagopal <https://github.com/sivagopal> I have the access token. I use
the following to get it:
command = "az account get-access-token --resource https://ossrdbms-aad.database.windows.net"
response = subprocess.check_output(command, shell=True)
token = json.loads(response)['accessToken']
print(len(token))
The length of the 'reponse' is 1942. I see that the 'response' is an
object with an attribute 'accessToken' but also others such as 'tenant' and
'expiresOn'. I've assumed I just need the 'accessToken'. The length of
'token' is 1738 and it begins with 'eyJ0...' .
@v-chojas <https://github.com/v-chojas> after the expansion loop the
length is 3480. So:
1738
eyJ0eXAiOiJK ...
And after expansion:
3480
b'\x94\r\x00\x00e\x00y ...
Does that look right? In either case the login fails. If I include
UID/PSSWD the error is:
Error: ('01000', "[01000] [unixODBC][Driver Manager]Can't open lib ***@***.***' : file not found (0) (SQLDriverConnect)")
If I don't include UID/PSSWD the error is as follows which looks like it
doesn't recognize the user(?!):
InterfaceError: ('28000', "[28000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Login failed for user ''. (18456) (SQLDriverConnect)")
This seems very difficult for what I expected to be straightforward. I
greatly appreciate
the help :-)
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#228 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AA6H6WGGRT5UIX4Q4PMSALTRHD5W3ANCNFSM4DHRUILQ>
.
|
Correct.
If the token ('eyJ0...' string) is 1738 bytes long, then expanding it should've resulted in 3476, and prepending the 4-byte length gets you to 3480, so if that's what you meant, it seems right to me. Use a connection string with only Driver, Server, and Database when connecting using the access token. The token is supplied as a connection attribute so you should not supply any other credentials. The "login failed for user '' " suggests the driver might not be new enough to support access token; an invalid token usually gives "login failed for 'NT AUTHORITY\ANONYMOUS LOGON'". Make sure you are using latest version of the ODBC driver. |
@Siva - an example might be good. When I run 'az account get-access...' the object returned has 'tokenType:Bearer', 'tenant', etc. I have only be using 'accessToken' and didn't think the rest was required. @v-chojas - I have the latest ODBC (17.5.1). But about the length, if the length is 3480 then should that be the value for SQL_COPT_SS_ACCESS_TOKEN instead of 1256?
Again, thank you for the time and advice. |
1256 is the identifier of the connection attribute, not the length of the access token. 17.5.1 is not the latest but should be new enough for access token support. |
I tried changing to "driver= '/usr/local/lib/libtdsodbc.so'" instead of "driver='ODBC Driver 17 for SQL Server'". When using that I get:
|
ODBC 17 Driver is good to go,,here is an example app I used:
headers = {"Metadata" : 'true'}
params = (('api-version', '2018-02-01'), ('resource', '
https://database.windows.net'))
response = requests.get('http://ipadress/metadata/identity/oauth2/token',
headers=headers, params= params)
token = response.json().get('access-token')
token = token.encode('utf-8')
now replace you token struct loop
CONNSTRING = "DRIVER=(ODBC Driver 17 for SQL Server);
SERVER=yourdatabaseserver; Authentication=ActiveDirectoryMsi (or specific
to your auth type, see azure documentation)"
params - urllib.quote_plus(CONNSTRING)\
DATABASE_URI="mssql+pyodbc://?odbc_connect=%s" % params
The above code works for me
…On Fri, Mar 13, 2020 at 4:31 PM MikeB2019x ***@***.***> wrote:
I tried changing to "driver= '/usr/local/lib/libtdsodbc.so'" instead of
"driver='ODBC Driver 17 for SQL Server'". When using that I get:
OperationalError: ('08S01', '[08S01] [FreeTDS][SQL Server]Unable to connect: Adaptive Server is unavailable or does not exist (20009) (SQLDriverConnect)')
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#228 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AA6H6WHKR3AWBRQT5VWA7T3RHJNURANCNFSM4DHRUILQ>
.
--
Thanks & Regards,
Sivagopal.B
|
If I use:
The error says "TypeError: can only concatenate str (not "int") to str". |
At least for a JWT access token -- which is what Azure AD vends - the UTF_16_LE encoding approach is both correct and very elegant. JWT is comprised of three BASE64 encoded sections joined by dots. Since BASE64 is a strict subset of ASCII and since the dot character is also ASCII, the entire payload will be ASCII. |
Hi, here is the code I am using conn = pyodbc.connect(connString, attrs_before = { SQL_COPT_SS_ACCESS_TOKEN:tokenstruct}); " TypeError: connection string too long |
That suggests you are trying to put the access token in the connection string (which it shouldn't be in), and are also using an older pyODBC version which had that limit on the connection string length. |
Hi @michaelcapizzi , thanks a lot for sharing these snippets. I want to know about this:
I also need to convince my DBA about setting this up. They're insisting that there's no such thing - that there's no linking the registered applications to DBs. How does one do this? Some documentation links, screenshots etc would be very helpful. The MS docs I've seen go deep into regsitering applications and all, but don't show how to link the application to MSSQL. |
You need to create a database user that corresponds to the identity of the token; see the docs for CREATE USER FROM EXTERNAL PROVIDE for more info. |
How to add permission for the app-registration to the specific database? |
I am also on the same page. How did you register or solved this issue? |
Has anyone ever seen an issue in performance degradation when connecting with an access_token vs username and password? From analysis it seems that pooling of connections is not working correctly on an azure sql database with an access token vs username and password. |
Just sayin' Prepending each byte with a zero byte, in a case where the original text is ASCII text (base64 + dot Effectively that counter-intuitive loop that I've been seeing everywhere, can be forgotten by just writing
in the first place, and not manually to UTF-8 and then add a zero byte to each byte. |
Yes, that will work provided you're input is ASCII. Note: don't forget to prepend the length of the string. See my However, as the maintainer states in #228 (comment), there is no guarantee that the input is ASCII. That's why I changed my solution to
There's as much looping in my solution as there is in yours 😉 |
It's base64 str. It is always ASCII. The struct packing is not solved by encoding in UTF-16-LE, but otherwise if you lose the ugly Python loop that tries to prepare data that's compatible with MSODBC driver (that uses UTF-16-LE internally apparently), and do explicitly what needs to be done (encoding in UTF-16-LE), rather than manually playing with bytes, then it works. |
The fact that you are currently seeing base64 strings as tokens is not a guarantee that that is what they will remain. The access token mechanism supports arbitrary binary data. I am not a maintainer of pyODBC, but I do maintain the official Microsoft ODBC Driver for SQL Server. |
Hi, no idea how it worked, but by using redhat's python image in the Dockerfile, the AzureAD login is working for me. I did not need to do any of this extra stuff; it connected to the DB in the same way non-AD connections happen; just with an extra "Authentication=ActiveDirectoryPassword" added in the connection string. Dockerfile:
Now I'm not sure if at your end you'll be able to pull this red hat docker image or not - pls try and see. In the program, credentials are loaded like so:
Note the "Authentication=ActiveDirectoryPassword" part at the end. Code in program, assuming above creds are loaded into a "params" dict:
So, somehow in the redhat image it looks like pyodbc is taking care of AD auth under-the-hood, while on using regular python image or ubuntu image like |
I can confirm that that the following works for me with
|
Has anyone tried using access tokens with Amazon RDS + SQL Server? I've tried all of the above suggestions but consistently get the error:
If I take the generated access token and paste it directly into some C# code that connects to the same RDS instance, it works fine. |
I'm getting [Microsoft][ODBC Driver 18 for SQL Server][SQL Server]Login failed for user '. I'm using my account which is probably admin. could this be the issue? |
Is it actually empty or does it say |
I was wondering if pyodbc will support connecting to an Azure SQL DB using the AD access token instead of user/password?
The text was updated successfully, but these errors were encountered: