Difference between revisions of "User:Rayanth/Sandbox3"
(Created page with "This page currently hosts some python code exploration to help a user of Tweetfleet with understanding how it goes through the Native-flow OAuth Authentication to SSO. Adapte...") |
|||
(2 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
+ | {{#css: | ||
+ | code { | ||
+ | color: #008F11; | ||
+ | } | ||
+ | }} | ||
+ | |||
+ | |||
This page currently hosts some python code exploration to help a user of Tweetfleet with understanding how it goes through the Native-flow OAuth Authentication to SSO. | This page currently hosts some python code exploration to help a user of Tweetfleet with understanding how it goes through the Native-flow OAuth Authentication to SSO. | ||
− | Adapted from the description of the native-flow process on https://github.com/esi/esi-docs/blob/master/docs/sso/native_sso_flow.md | + | Adapted from the description of the native-flow process on https://github.com/esi/esi-docs/blob/master/docs/sso/native_sso_flow.md<br> |
− | and its associated python code examples at https://github.com/esi/esi-docs/tree/master/examples/python/sso | + | and its associated python code examples at https://github.com/esi/esi-docs/tree/master/examples/python/sso <br> |
(notably, "esi_oauth_natice.py" and "shared_flow.py") | (notably, "esi_oauth_natice.py" and "shared_flow.py") | ||
=Flow= | =Flow= | ||
+ | |||
+ | per the docs, the flow follows this process: | ||
+ | * create a 32-byte random string | ||
+ | * base64 urlencode that string | ||
+ | * sha256 hash that | ||
+ | * base64 urlencode the hash | ||
+ | * send the hash to SSO as a code_challenge | ||
+ | then after directing the user to log in and getting callback | ||
+ | * take the original base64 urlencoded string (pre-hash) | ||
+ | * urlencode THAT | ||
+ | * send it to SSO as code_verifier with the token | ||
+ | |||
+ | =The code= | ||
+ | |||
+ | Rather than paste the code of https://github.com/esi/esi-docs/blob/master/examples/python/sso/esi_oauth_native.py here, i'll use a breakdown i ran in python console, using that code with actual values: | ||
+ | |||
+ | We start with general imports. these are part of python base library, so no need to pip install: | ||
+ | <pre> | ||
+ | import base64 | ||
+ | import hashlib | ||
+ | import secrets | ||
+ | </pre> | ||
+ | The initial stage is to generate our code_challenge. We'll use a couple variables to watch the process along the way: | ||
+ | <pre> | ||
+ | secret = secrets.token_bytes(32) | ||
+ | print(secret) | ||
+ | </pre> | ||
+ | python console output:<br> | ||
+ | <code>b'\x00\x06\xc4\x18\xa4\\J\x028\xe4\x14/\r\xcd8\xba\xf2\xb6\xd7\x07v#\x00E&\xdb`\xeei\x9f\xeb\xce'</code><br> | ||
+ | This represents a byte-string, similar to a byte array in other languages. some bytes happen to fall in the readable range, others are shown as hex value. | ||
+ | |||
+ | Next, we need to base64 urlencode this. python provides that function in one: | ||
+ | <pre> | ||
+ | random = base64.urlsafe_b64encode(secret) | ||
+ | print(random) | ||
+ | </pre> | ||
+ | Take careful note of the variable 'random' -- we'll be using it later in the code_verifier. this is AFTER it has been base64_urlencoded once.<br> | ||
+ | python console output:<br> | ||
+ | <code>b'AAbEGKRcSgI45BQvDc04uvK21wd2IwBFJttg7mmf684='</code><br> | ||
+ | Again, this string is in bytes representation, it's just all urlsafe so it's all legible. the python SHA256 library wants a "bytes like object" so we leave it in this form. | ||
+ | |||
+ | now to hash it with SHA256:<br> | ||
+ | <pre> | ||
+ | m = hashlib.sha256() # make a hashed variable | ||
+ | m.update(random) # hash 'random' into it | ||
+ | d = m.digest() # return the digest | ||
+ | print(d) | ||
+ | </pre> | ||
+ | Python console output:<br> | ||
+ | <code>b'*}\x89k \xf5\x8f\x8a\xe3P\x97\xff\xe5\xd9\x19F\xe5\xab9\xae\x9a\xa8(\xfe\xd5Q\xc0\x11\xfb\xd6\xa0\x8c'</code><br> | ||
+ | Once again we're in semi-representable bytestring mode. So we need to base64 URL-encode THIS next. | ||
+ | <pre> | ||
+ | urlsafe = base64.urlsafe_b64encode(d) | ||
+ | print(urlsafe) | ||
+ | <pre> | ||
+ | Python console output:<br> | ||
+ | <code>b'Kn2JayD1j4rjUJf_5dkZRuWrOa6aqCj-1VHAEfvWoIw='</code><br> | ||
+ | This is still byte-string, so let's make it a string with decode()<br> | ||
+ | <pre> | ||
+ | decoded = urlsafe.decode() | ||
+ | print(decoded) | ||
+ | </pre> | ||
+ | Python console output:<br> | ||
+ | <code>Kn2JayD1j4rjUJf_5dkZRuWrOa6aqCj-1VHAEfvWoIw=</code><br> | ||
+ | Note the only difference was the variable type, it's now an actual string. But the = sign on the end could be problematic if we send it as a parameter in the URL string rather than in form fields.<br> | ||
+ | One feature of Base64 encoding is that it always results in a string with a length that is a multiple of 4. the = on the end is just padding to make sure it's of a multiple of 4 in length. The server knows this, so if we send the string without the right length, it will pad the string again before decoding it. So let's just chop the = off: | ||
+ | <pre> | ||
+ | code_challenge = decoded.replace("=", "") | ||
+ | print(code_challenge) | ||
+ | </pre> | ||
+ | Python console output:<br> | ||
+ | <code>Kn2JayD1j4rjUJf_5dkZRuWrOa6aqCj-1VHAEfvWoIw</code><br> | ||
+ | Again, the only thing that changed is we removed the = ... and this is now our Code-Challenge string that we can send to SSO.<br> | ||
+ | The constructed URL looks like: | ||
+ | <pre>https://login.eveonline.com/v2/oauth/authorize/?response_type=code&redirect_uri=https%3A%2F%2Flocalhost%2Fcallback%2F&client_id=12345abcde&state=unique-state&code_challenge=Kn2JayD1j4rjUJf_5dkZRuWrOa6aqCj-1VHAEfvWoIw&code_challenge_method=S256 </pre> | ||
+ | Now, let us assume that we've got the callback and the auth_code from that. We need to start the second phase, which includes a code_verifier to tell the server we're the same app that sent the user to them in the first place. This is done by sending the original random string without hashing it, and the server will compare it to the hashed version we sent along with the user the first time. | ||
+ | |||
+ | Here is how that looks, broken down.<br> | ||
+ | REMEMBER: We are using the 'random' that we stored ''before'' we hashed it in the first stage, but ''after'' we base64 urlencoded it. | ||
+ | <pre> | ||
+ | temp = base64.urlsafe_b64encode(random) | ||
+ | print(temp) | ||
+ | </pre> | ||
+ | Python console output:<br> | ||
+ | <code>b'QUFiRUdLUmNTZ0k0NUJRdkRjMDR1dksyMXdkMkl3QkZKdHRnN21tZjY4ND0='</code><br> | ||
+ | Why is it different than before? Because we base64_urlencoded a string that has ''already been'' base64_urlencoded before, in the initial stage.<br> | ||
+ | And we need to turn it into a string, so decode(): | ||
+ | <pre> | ||
+ | temp_decoded = temp.decode() | ||
+ | print(temp_decoded) | ||
+ | </pre> | ||
+ | Python console output:<br> | ||
+ | <code>QUFiRUdLUmNTZ0k0NUJRdkRjMDR1dksyMXdkMkl3QkZKdHRnN21tZjY4ND0=</code><br> | ||
+ | Chop off the = sign... | ||
+ | <pre> | ||
+ | code_verifier = temp_decoded.replace("=", "") | ||
+ | print(code_verifier) | ||
+ | </pre> | ||
+ | Python console output:<br> | ||
+ | <code>QUFiRUdLUmNTZ0k0NUJRdkRjMDR1dksyMXdkMkl3QkZKdHRnN21tZjY4ND0</code><br> | ||
+ | and now <code>code_verifier</code> contains what we will send in the back-end request to SSO: | ||
+ | <pre> | ||
+ | form_values = { | ||
+ | "grant_type": "authorization_code", | ||
+ | "client_id": client_id, # as above, I used '12345abcde' | ||
+ | "code": auth_code, # from the callback | ||
+ | "code_verifier": code_verifier # just calculated. | ||
+ | } | ||
+ | </pre> | ||
+ | <code>auth_code</code> is what was returned in the callback from SSO when the user logged in,<br> | ||
+ | <code>client_id</code> should be your SSO client_id as sent the first time with the user (i used 12345abcde just as an example),<br> | ||
+ | <code>code_verifier</code> is the code we just generated in this second phase - the original base64_urlencoded random string, run through base64_urlencoding ''again'' |
Latest revision as of 20:45, 8 January 2020
This page currently hosts some python code exploration to help a user of Tweetfleet with understanding how it goes through the Native-flow OAuth Authentication to SSO.
Adapted from the description of the native-flow process on https://github.com/esi/esi-docs/blob/master/docs/sso/native_sso_flow.md
and its associated python code examples at https://github.com/esi/esi-docs/tree/master/examples/python/sso
(notably, "esi_oauth_natice.py" and "shared_flow.py")
Flow
per the docs, the flow follows this process:
- create a 32-byte random string
- base64 urlencode that string
- sha256 hash that
- base64 urlencode the hash
- send the hash to SSO as a code_challenge
then after directing the user to log in and getting callback
- take the original base64 urlencoded string (pre-hash)
- urlencode THAT
- send it to SSO as code_verifier with the token
The code
Rather than paste the code of https://github.com/esi/esi-docs/blob/master/examples/python/sso/esi_oauth_native.py here, i'll use a breakdown i ran in python console, using that code with actual values:
We start with general imports. these are part of python base library, so no need to pip install:
import base64 import hashlib import secrets
The initial stage is to generate our code_challenge. We'll use a couple variables to watch the process along the way:
secret = secrets.token_bytes(32) print(secret)
python console output:
b'\x00\x06\xc4\x18\xa4\\J\x028\xe4\x14/\r\xcd8\xba\xf2\xb6\xd7\x07v#\x00E&\xdb`\xeei\x9f\xeb\xce'
This represents a byte-string, similar to a byte array in other languages. some bytes happen to fall in the readable range, others are shown as hex value.
Next, we need to base64 urlencode this. python provides that function in one:
random = base64.urlsafe_b64encode(secret) print(random)
Take careful note of the variable 'random' -- we'll be using it later in the code_verifier. this is AFTER it has been base64_urlencoded once.
python console output:
b'AAbEGKRcSgI45BQvDc04uvK21wd2IwBFJttg7mmf684='
Again, this string is in bytes representation, it's just all urlsafe so it's all legible. the python SHA256 library wants a "bytes like object" so we leave it in this form.
now to hash it with SHA256:
m = hashlib.sha256() # make a hashed variable m.update(random) # hash 'random' into it d = m.digest() # return the digest print(d)
Python console output:
b'*}\x89k \xf5\x8f\x8a\xe3P\x97\xff\xe5\xd9\x19F\xe5\xab9\xae\x9a\xa8(\xfe\xd5Q\xc0\x11\xfb\xd6\xa0\x8c'
Once again we're in semi-representable bytestring mode. So we need to base64 URL-encode THIS next.
urlsafe = base64.urlsafe_b64encode(d) print(urlsafe) <pre> Python console output:<br> <code>b'Kn2JayD1j4rjUJf_5dkZRuWrOa6aqCj-1VHAEfvWoIw='</code><br> This is still byte-string, so let's make it a string with decode()<br> <pre> decoded = urlsafe.decode() print(decoded)
Python console output:
Kn2JayD1j4rjUJf_5dkZRuWrOa6aqCj-1VHAEfvWoIw=
Note the only difference was the variable type, it's now an actual string. But the = sign on the end could be problematic if we send it as a parameter in the URL string rather than in form fields.
One feature of Base64 encoding is that it always results in a string with a length that is a multiple of 4. the = on the end is just padding to make sure it's of a multiple of 4 in length. The server knows this, so if we send the string without the right length, it will pad the string again before decoding it. So let's just chop the = off:
code_challenge = decoded.replace("=", "") print(code_challenge)
Python console output:
Kn2JayD1j4rjUJf_5dkZRuWrOa6aqCj-1VHAEfvWoIw
Again, the only thing that changed is we removed the = ... and this is now our Code-Challenge string that we can send to SSO.
The constructed URL looks like:
https://login.eveonline.com/v2/oauth/authorize/?response_type=code&redirect_uri=https%3A%2F%2Flocalhost%2Fcallback%2F&client_id=12345abcde&state=unique-state&code_challenge=Kn2JayD1j4rjUJf_5dkZRuWrOa6aqCj-1VHAEfvWoIw&code_challenge_method=S256
Now, let us assume that we've got the callback and the auth_code from that. We need to start the second phase, which includes a code_verifier to tell the server we're the same app that sent the user to them in the first place. This is done by sending the original random string without hashing it, and the server will compare it to the hashed version we sent along with the user the first time.
Here is how that looks, broken down.
REMEMBER: We are using the 'random' that we stored before we hashed it in the first stage, but after we base64 urlencoded it.
temp = base64.urlsafe_b64encode(random) print(temp)
Python console output:
b'QUFiRUdLUmNTZ0k0NUJRdkRjMDR1dksyMXdkMkl3QkZKdHRnN21tZjY4ND0='
Why is it different than before? Because we base64_urlencoded a string that has already been base64_urlencoded before, in the initial stage.
And we need to turn it into a string, so decode():
temp_decoded = temp.decode() print(temp_decoded)
Python console output:
QUFiRUdLUmNTZ0k0NUJRdkRjMDR1dksyMXdkMkl3QkZKdHRnN21tZjY4ND0=
Chop off the = sign...
code_verifier = temp_decoded.replace("=", "") print(code_verifier)
Python console output:
QUFiRUdLUmNTZ0k0NUJRdkRjMDR1dksyMXdkMkl3QkZKdHRnN21tZjY4ND0
and now code_verifier
contains what we will send in the back-end request to SSO:
form_values = { "grant_type": "authorization_code", "client_id": client_id, # as above, I used '12345abcde' "code": auth_code, # from the callback "code_verifier": code_verifier # just calculated. }
auth_code
is what was returned in the callback from SSO when the user logged in,
client_id
should be your SSO client_id as sent the first time with the user (i used 12345abcde just as an example),
code_verifier
is the code we just generated in this second phase - the original base64_urlencoded random string, run through base64_urlencoding again