codecov[bot]: # [Codecov](https://codecov.io/gh/alexgolec/tda-api/pull/37?src=pr&el=h1) Report
> Merging [#37](https://codecov.io/gh/alexgolec/tda-api/pull/37?src=pr&el=desc) into [master](https://codecov.io/gh/alexgolec/tda-api/commit/c9a9a9e0ff215aec66af2a622170e0aad8558a5f&el=desc) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/alexgolec/tda-api/pull/37/graphs/tree.svg?width=650&height=150&src=pr&token=M8H98HA8CK)](https://codecov.io/gh/alexgolec/tda-api/pull/37?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master      #37   +/-   ##
=======================================
  Coverage   99.31%   99.31%           
=======================================
  Files          11       11           
  Lines        1600     1601    +1     
=======================================
+ Hits         1589     1590    +1     
  Misses         11       11           
```


| [Impacted Files](https://codecov.io/gh/alexgolec/tda-api/pull/37?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tda/auth.py](https://codecov.io/gh/alexgolec/tda-api/pull/37/diff?src=pr&el=tree#diff-dGRhL2F1dGgucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/alexgolec/tda-api/pull/37?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/alexgolec/tda-api/pull/37?src=pr&el=footer). Last update [c9a9a9e...090dc7d](https://codecov.io/gh/alexgolec/tda-api/pull/37?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/tda/auth.py b/tda/auth.py
index d9c7976..105dadb 100644
--- a/tda/auth.py
+++ b/tda/auth.py
@@ -58,6 +58,7 @@ def client_from_token_file(token_path, api_key):
     __register_token_redactions(token)
 
     # Return a new session configured to refresh credentials
+    api_key = __normalize_api_key(api_key)
     return Client(
         api_key,
         OAuth2Session(api_key, token=token,
@@ -89,6 +90,8 @@ def client_from_login_flow(webdriver, api_key, redirect_url, token_path,
     get_logger().info(('Creating new token with redirect URL \'{}\' ' +
                        'and token path \'{}\'').format(redirect_url, token_path))
 
+    api_key = __normalize_api_key(api_key)
+
     oauth = OAuth2Session(api_key, redirect_uri=redirect_url)
     authorization_url, state = oauth.authorization_url(
         'https://auth.tdameritrade.com/auth')
@@ -155,7 +158,6 @@ def easy_client(api_key, redirect_uri, token_path, webdriver_func=None):
                            a new token. Will only be called if the token file
                            cannot be found.
     '''
-    api_key = __normalize_api_key(api_key)
     logger = get_logger()
 
     try:

authentication failure
Hey

I've created an app on TD Ameritrade with a callback URL `https://127.0.0.1:8080` (as [suggested](https://developer.tdameritrade.com/content/authentication-faq) on developer pages). I am making sure I am sending the correct API_KEY and redirect_url to the authentication flow

```
with webdriver.Chrome() as driver:
    c = auth.client_from_login_flow(driver, apikey, redirect_uri, token_path)
```

However, I am getting the error page `a third party application` [you document](https://tda-api.readthedocs.io/en/latest/auth.html). 

Logging shows the port on the localhost being sent with the GET is different however

```
2020-06-08 16:48:47,541 DEBUG    GET http://127.0.0.1:57214/session/<SESSION>/url {}
2020-06-08 16:48:47,544 DEBUG    http://127.0.0.1:57214 "GET /session/<SESSION>/url HTTP/1.1" 200 189
2020-06-08 16:48:47,544 DEBUG    Finished Request
```
diff --git a/tests/auth_test.py b/tests/auth_test.py
index d9457ed..ebf9d43 100644
--- a/tests/auth_test.py
+++ b/tests/auth_test.py
@@ -9,7 +9,7 @@ import tempfile
 import unittest
 
 
-API_KEY = 'APIKEY'
+API_KEY = 'APIKEY@AMER.OAUTHAP'
 
 
 class ClientFromTokenFileTest(unittest.TestCase):
@@ -46,6 +46,24 @@ class ClientFromTokenFileTest(unittest.TestCase):
             auto_refresh_kwargs=_,
             token_updater=_)
 
+    @no_duplicates
+    @patch('tda.auth.Client')
+    @patch('tda.auth.OAuth2Session')
+    def test_api_key_is_normalized(self, session, client):
+        self.write_token()
+
+        client.return_value = 'returned client'
+
+        self.assertEqual('returned client',
+                         auth.client_from_token_file(self.pickle_path, 'API_KEY'))
+        client.assert_called_once_with('API_KEY@AMER.OAUTHAP', _)
+        session.assert_called_once_with(
+            'API_KEY@AMER.OAUTHAP',
+            token=self.token,
+            auto_refresh_url=_,
+            auto_refresh_kwargs=_,
+            token_updater=_)
+
 
 REDIRECT_URL = 'https://redirect.url.com'
 
@@ -82,6 +100,32 @@ class ClientFromLoginFlow(unittest.TestCase):
         with open(self.pickle_path, 'rb') as f:
             self.assertEqual(self.token, pickle.load(f))
 
+    @no_duplicates
+    @patch('tda.auth.Client')
+    @patch('tda.auth.OAuth2Session')
+    def test_normalize_api_key(self, session_constructor, client):
+        AUTH_URL = 'https://auth.url.com'
+
+        session = MagicMock()
+        session_constructor.return_value = session
+        session.authorization_url.return_value = AUTH_URL, None
+        session.fetch_token.return_value = self.token
+
+        webdriver = MagicMock()
+        webdriver.get.return_value = REDIRECT_URL + '/token_params'
+
+        client.return_value = 'returned client'
+
+        self.assertEqual('returned client',
+                         auth.client_from_login_flow(
+                             webdriver, 'API_KEY', REDIRECT_URL,
+                             self.pickle_path,
+                             redirect_wait_time_seconds=0.0))
+
+        self.assertEqual(
+                'API_KEY@AMER.OAUTHAP',
+                session_constructor.call_args[0][0])
+
 
 class EasyClientTest(unittest.TestCase):
 

pip install -e .
parse_log_pytest
requirements.txt
nose pytest
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
attrs==25.3.0
babel==2.17.0
backports.tarfile==1.2.0
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
colorama==0.4.6
coverage==7.8.0
cryptography==44.0.2
docutils==0.21.2
exceptiongroup==1.2.2
h11==0.14.0
id==1.5.0
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
iniconfig==2.1.0
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jeepney==0.9.0
Jinja2==3.1.6
keyring==25.6.0
markdown-it-py==3.0.0
MarkupSafe==3.0.2
mdurl==0.1.2
more-itertools==10.6.0
nh3==0.2.21
nose==1.3.7
oauthlib==3.2.2
outcome==1.3.0.post0
packaging==24.2
pluggy==1.5.0
pycparser==2.22
Pygments==2.19.1
PySocks==1.7.1
pytest==8.3.5
python-dateutil==2.9.0.post0
pytz==2025.2
readme_renderer==44.0
requests==2.32.3
requests-oauthlib==2.0.0
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.0.0
SecretStorage==3.3.3
selenium==4.30.0
six==1.17.0
sniffio==1.3.1
snowballstemmer==2.2.0
sortedcontainers==2.4.0
Sphinx==7.4.7
sphinx-rtd-theme==3.0.2
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
-e git+https://github.com/alexgolec/tda-api.git@c9a9a9e0ff215aec66af2a622170e0aad8558a5f#egg=tda_api
tomli==2.2.1
trio==0.29.0
trio-websocket==0.12.2
twine==6.1.0
typing_extensions==4.13.0
urllib3==2.3.0
websocket-client==1.8.0
websockets==15.0.1
wsproto==1.2.0
zipp==3.21.0

name: tda-api
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - attrs==25.3.0
      - babel==2.17.0
      - backports-tarfile==1.2.0
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - colorama==0.4.6
      - coverage==7.8.0
      - cryptography==44.0.2
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - h11==0.14.0
      - id==1.5.0
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - jaraco-classes==3.4.0
      - jaraco-context==6.0.1
      - jaraco-functools==4.1.0
      - jeepney==0.9.0
      - jinja2==3.1.6
      - keyring==25.6.0
      - markdown-it-py==3.0.0
      - markupsafe==3.0.2
      - mdurl==0.1.2
      - more-itertools==10.6.0
      - nh3==0.2.21
      - nose==1.3.7
      - oauthlib==3.2.2
      - outcome==1.3.0.post0
      - packaging==24.2
      - pluggy==1.5.0
      - pycparser==2.22
      - pygments==2.19.1
      - pysocks==1.7.1
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - readme-renderer==44.0
      - requests==2.32.3
      - requests-oauthlib==2.0.0
      - requests-toolbelt==1.0.0
      - rfc3986==2.0.0
      - rich==14.0.0
      - secretstorage==3.3.3
      - selenium==4.30.0
      - six==1.17.0
      - sniffio==1.3.1
      - snowballstemmer==2.2.0
      - sortedcontainers==2.4.0
      - sphinx==7.4.7
      - sphinx-rtd-theme==3.0.2
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - tomli==2.2.1
      - trio==0.29.0
      - trio-websocket==0.12.2
      - twine==6.1.0
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - websocket-client==1.8.0
      - websockets==15.0.1
      - wsproto==1.2.0
      - zipp==3.21.0
prefix: /opt/conda/envs/tda-api

tests/auth_test.py::ClientFromTokenFileTest::test_api_key_is_normalized tests/auth_test.py::ClientFromLoginFlow::test_normalize_api_key
tests/auth_test.py::ClientFromTokenFileTest::test_file_exists tests/auth_test.py::ClientFromTokenFileTest::test_no_such_file tests/auth_test.py::ClientFromLoginFlow::test_no_token_file tests/auth_test.py::EasyClientTest::test_no_token_file_no_wd_func tests/auth_test.py::EasyClientTest::test_no_token_file_with_wd_func tests/auth_test.py::EasyClientTest::test_token_file
tda/auth.py
tda/auth.py