From f59ae0eff6c46e12ab1ff16c3a0ccda0870f7675 Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Sun, 28 Jan 2024 22:09:33 -0600 Subject: [PATCH 1/3] Add validate_token() function --- HISTORY.md | 1 + pyinaturalist/auth.py | 14 ++++++++++++-- test/test_auth.py | 14 +++++++++++++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 64984e36..bbf133ab 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,7 @@ * ⚠️ Drop support for python 3.7 * Fix `KeyError` when using `create_observation()` in dry-run mode * Increase default request timeout from 10 to 20 seconds +* Add `validate_token()` function to manually check if an access token is valid ## 0.19.0 (2023-12-12) diff --git a/pyinaturalist/auth.py b/pyinaturalist/auth.py index a21d980a..934acdb1 100644 --- a/pyinaturalist/auth.py +++ b/pyinaturalist/auth.py @@ -4,9 +4,9 @@ from keyring import get_password, set_password from keyring.errors import KeyringError -from requests import Response +from requests import HTTPError, Response -from pyinaturalist.constants import API_V0, KEYRING_KEY +from pyinaturalist.constants import API_V0, API_V1, KEYRING_KEY from pyinaturalist.exceptions import AuthenticationError from pyinaturalist.session import ClientSession, get_local_session @@ -106,6 +106,16 @@ def get_access_token( return access_token +def validate_token(access_token: str) -> bool: + """Determine if an access token is valid""" + session = get_local_session() + try: + session.request('GET', f'{API_V1}/users/me', access_token=access_token) + return True + except HTTPError: + return False + + def get_keyring_credentials() -> Dict[str, Optional[str]]: """Attempt to get iNaturalist credentials from the system keyring diff --git a/test/test_auth.py b/test/test_auth.py index 8e0217b2..d04fdc64 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -6,7 +6,12 @@ from keyring.errors import KeyringError from requests import HTTPError, Response -from pyinaturalist.auth import get_access_token, get_keyring_credentials, set_keyring_credentials +from pyinaturalist.auth import ( + get_access_token, + get_keyring_credentials, + set_keyring_credentials, + validate_token, +) from pyinaturalist.constants import API_V0 from pyinaturalist.exceptions import AuthenticationError from pyinaturalist.session import ClientSession @@ -154,6 +159,13 @@ def test_get_access_token__invalid_creds(mock_get_jwt, requests_mock): get_access_token('username', 'password', 'app_id', 'app_secret') +@pytest.mark.parametrize('response_code', [200, 401, 403, 502]) +def test_validate_token(response_code): + with patch.object(ClientSession, 'send', return_value=Response()) as mock_get: + mock_get.return_value.status_code = response_code + assert validate_token('token') == (response_code == 200) + + # get_keyring_credentials # ------------------------- From 903c10c4ae90f055dcda92b3defee9353f6fed31 Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Sun, 28 Jan 2024 22:11:23 -0600 Subject: [PATCH 2/3] Add note to docs about manually getting an access token from a browser --- docs/user_guide/advanced.md | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/user_guide/advanced.md b/docs/user_guide/advanced.md index d86d7bb6..2415c744 100644 --- a/docs/user_guide/advanced.md +++ b/docs/user_guide/advanced.md @@ -16,18 +16,32 @@ Note that this replaces the `get_all_*()` functions from pyinaturalist\<=0.12. (auth)= ## Authentication For any endpoints that create, update, or delete data, you will need to authenticate using an -OAuth2 access token. This requires both your iNaturalist username and password, and separate -"application" credentials. +access token. There are two main ways to get one: manually via a browser, or programatically, using +your iNaturalist credentials plus separate application credentials. These tokens are typically valid +for 24 hours, after which you will need to get a new one. + +You can then pass the access token to any API request function that uses it via the +`access_token` argument. For example: +```py +from pyinaturalist import create_observation + +create_observation( + ..., + access_token='my_access_token', +) +``` :::{note} Read-only requests generally don't require authentication; however, if you want to access -private data visible only to your user (for example, obscured or private coordinates), -you will need to use an access token. +private data visible only to your user (for example, obscured or private coordinates), you will need +to use an access token. ::: -**Summary:** -1. Create an iNaturalist application -2. Use {py:func}`.get_access_token` with your user + application credentials to get an access token -3. Pass that access token to any API request function that uses it +### Manual Authentication +You can get an access token from a browser by logging in to inaturalist.org and going to +https://www.inaturalist.org/users/api_token. + +This has the advantage of not needing to create an iNaturalist application (see section below), but +has the disadvantage of requiring you to manually copy and paste the token. ### Creating an Application :::{dropdown} Why do I need to create an application? From 6740518928b0a7ded6d1840b715eb22898779d4c Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Sun, 28 Jan 2024 22:19:56 -0600 Subject: [PATCH 3/3] Move authentication docs to their own page --- README.md | 4 +- docs/user_guide/advanced.md | 188 +------------------------ docs/user_guide/authentication.md | 183 ++++++++++++++++++++++++ docs/user_guide/index.md | 1 + examples/observation_photo_metadata.py | 2 +- 5 files changed, 190 insertions(+), 188 deletions(-) create mode 100644 docs/user_guide/authentication.md diff --git a/README.md b/README.md index 2033403d..d38be74a 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ token = get_access_token( app_secret='my_app_secret', ) ``` -See [Authentication](https://pyinaturalist.readthedocs.io/en/stable/user_guide.html#authentication) +See [Authentication](https://pyinaturalist.readthedocs.io/en/stable/authentication.md) for more options including environment variables, keyrings, and password managers. Now we can [create a new observation](https://pyinaturalist.readthedocs.io/en/stable/modules/pyinaturalist.v1.observations.html#pyinaturalist.v1.observations.create_observation): @@ -183,7 +183,7 @@ As with observations, there is a lot of information in the response, but we'll p ## Next Steps For more information, see: -* [User Guide](https://pyinaturalist.readthedocs.io/en/stable/user_guide.html): +* [User Guide](https://pyinaturalist.readthedocs.io/en/stable/user_guide/index.html): introduction and general features that apply to most endpoints * [Endpoint Summary](https://pyinaturalist.readthedocs.io/en/stable/endpoints.html): a complete list of endpoints wrapped by pyinaturalist diff --git a/docs/user_guide/advanced.md b/docs/user_guide/advanced.md index 2415c744..ee819f83 100644 --- a/docs/user_guide/advanced.md +++ b/docs/user_guide/advanced.md @@ -1,6 +1,9 @@ # Advanced Usage This page describes some advanced features of pyinaturalist. +## Authentication +See {ref}`auth` for details on using authenticated endpoints. + ## Pagination Most endpoints support pagination, using the parameters: * `page`: Page number to get @@ -13,191 +16,6 @@ The default and maximum `per_page` values vary by endpoint, but it's 200 for mos To get all pages of results and combine them into a single response, use `page='all'`. Note that this replaces the `get_all_*()` functions from pyinaturalist\<=0.12. -(auth)= -## Authentication -For any endpoints that create, update, or delete data, you will need to authenticate using an -access token. There are two main ways to get one: manually via a browser, or programatically, using -your iNaturalist credentials plus separate application credentials. These tokens are typically valid -for 24 hours, after which you will need to get a new one. - -You can then pass the access token to any API request function that uses it via the -`access_token` argument. For example: -```py -from pyinaturalist import create_observation - -create_observation( - ..., - access_token='my_access_token', -) -``` - -:::{note} Read-only requests generally don't require authentication; however, if you want to access -private data visible only to your user (for example, obscured or private coordinates), you will need -to use an access token. -::: - -### Manual Authentication -You can get an access token from a browser by logging in to inaturalist.org and going to -https://www.inaturalist.org/users/api_token. - -This has the advantage of not needing to create an iNaturalist application (see section below), but -has the disadvantage of requiring you to manually copy and paste the token. - -### Creating an Application -:::{dropdown} Why do I need to create an application? -:icon: info - -iNaturalist uses OAuth2, which provides several different methods (or "flows") to access the site. -For example, on the [login page](https://www.inaturalist.org/login), you have the option of logging -in with a username/password, or with an external provider (Google, Facebook, etc.): - -```{image} ../images/inat-user-login.png -:alt: Login form -:width: 150 -``` - -Outside of iNaturalist.org, anything else that uses the API to create or modify data is considered -an "application," even if you're just running some scripts on your own computer. - -See [iNaturalist documentation](https://www.inaturalist.org/pages/api+reference#auth) -for more details on authentication. -::: - -First, go to [New Application](https://www.inaturalist.org/oauth/applications/new) and fill out the -following pieces of information: - -* **Name:** Any name you want to come up with. For example, if this is associated with a GitHub repo, - you can use your repo name. -* **Description:** A brief description of what you'll be using this for. For example, - *"Data access for my own observations"*. -* **Confidential:** ✔️ This should be checked. -* **URL and Redirect URI:** Just enter the URL to your GitHub repo, if you have one; otherwise any - placeholder like "" will work. - -```{image} ../images/inat-new-application.png -:alt: New Application form -:width: 300 -``` - -You should then see a screen like this, which will show your new application ID and secret. These will -only be shown once, so save them somewhere secure, preferably in a password manager. -```{image} ../images/inat-new-application-complete.png -:alt: Completed application form -:width: 400 -``` - -### Basic Usage -There are a few different ways you can pass your credentials to iNaturalist. First, you can pass -them as keyword arguments to {py:func}`.get_access_token`: - -```python ->>> from pyinaturalist import get_access_token ->>> access_token = get_access_token( ->>> username='my_inaturalist_username', # Username you use to login to iNaturalist.org ->>> password='my_inaturalist_password', # Password you use to login to iNaturalist.org ->>> app_id='33f27dc63bdf27f4ca6cd95dd', # OAuth2 application ID ->>> app_secret='bbce628be722bfe2abde4', # OAuth2 application secret ->>> ) -``` - -### Environment Variables - -You can also provide credentials via environment variables instead of arguments. The -environment variable names are the keyword arguments in uppercase, prefixed with `INAT_`: - -* `INAT_USERNAME` -* `INAT_PASSWORD` -* `INAT_APP_ID` -* `INAT_APP_SECRET` - -**Examples:** - -::::{tab-set} -:::{tab-item} Python -:sync: python - -```python ->>> import os ->>> os.environ['INAT_USERNAME'] = 'my_inaturalist_username' ->>> os.environ['INAT_PASSWORD'] = 'my_inaturalist_password' ->>> os.environ['INAT_APP_ID'] = '33f27dc63bdf27f4ca6cd95df' ->>> os.environ['INAT_APP_SECRET'] = 'bbce628be722bfe283de4' -``` -::: -:::{tab-item} Unix (MacOS / Linux) -:sync: unix - -```bash -export INAT_USERNAME="my_inaturalist_username" -export INAT_PASSWORD="my_inaturalist_password" -export INAT_APP_ID="33f27dc63bdf27f4ca6cd95df" -export INAT_APP_SECRET="bbce628be722bfe283de4" -``` -::: -:::{tab-item} Windows CMD -:sync: cmd - -```bat -set INAT_USERNAME="my_inaturalist_username" -set INAT_PASSWORD="my_inaturalist_password" -set INAT_APP_ID="33f27dc63bdf27f4ca6cd95df" -set INAT_APP_SECRET="bbce628be722bfe283de4" -``` -::: -:::{tab-item} PowerShell -:sync: ps1 - -```powershell -$Env:INAT_USERNAME="my_inaturalist_username" -$Env:INAT_PASSWORD="my_inaturalist_password" -$Env:INAT_APP_ID="33f27dc63bdf27f4ca6cd95df" -$Env:INAT_APP_SECRET="bbce628be722bfe283de4" -``` -::: -:::: - -Note that in any shell, these environment variables will only be set for your current shell -session. I.e., you can't set them in one terminal and then access them in another. - -### Keyring Integration -To handle your credentials more securely, you can store them in your system keyring. -You could manually store and retrieve them with a utility like -[secret-tool](https://manpages.ubuntu.com/manpages/xenial/man1/secret-tool.1.html) -and place them in environment variables as described above, but there is a much simpler option. - -Direct keyring integration is provided via [python keyring](https://github.com/jaraco/keyring). Most common keyring bakcends are supported, including: - -* macOS [Keychain](https://en.wikipedia.org/wiki/Keychain_%28software%29) -* Freedesktop [Secret Service](http://standards.freedesktop.org/secret-service/) -* KDE [KWallet](https://en.wikipedia.org/wiki/KWallet) -* [Windows Credential Locker](https://docs.microsoft.com/en-us/windows/uwp/security/credential-locker) - -To store your credentials in the keyring, run {py:func}`.set_keyring_credentials`: -```python ->>> from pyinaturalist.auth import set_keyring_credentials ->>> set_keyring_credentials( ->>> username='my_inaturalist_username', ->>> password='my_inaturalist_password', ->>> app_id='33f27dc63bdf27f4ca6cd95df', ->>> app_secret='bbce628be722bfe283de4', ->>> ) -``` - -Afterward, you can call {py:func}`.get_access_token` without any arguments, and your credentials -will be retrieved from the keyring. You do not need to run {py:func}`.set_keyring_credentials` -again unless you change your iNaturalist password. - -### Password Manager Integration -Keyring integration can be taken a step further by managing your keyring with a password -manager. This has the advantage of keeping your credentials in one place that can be synced -across multiple machines. [KeePassXC](https://keepassxc.org/) offers this feature for -macOS and Linux systems. See this guide for setup info: -[KeepassXC and secret service, a small walk-through](https://avaldes.co/2020/01/28/secret-service-keepassxc.html). - -```{figure} ../images/password_manager_keying.png -Credentials storage with keyring + KeePassXC -``` - ## Sessions If you want more control over how requests are sent, you can provide your own {py:class}`.ClientSession` object using the `session` argument for any API request function: diff --git a/docs/user_guide/authentication.md b/docs/user_guide/authentication.md new file mode 100644 index 00000000..b303c8e4 --- /dev/null +++ b/docs/user_guide/authentication.md @@ -0,0 +1,183 @@ +(auth)= +# Authentication +For any endpoints that create, update, or delete data, you will need to authenticate using an +access token. There are two main ways to get one: manually via a browser, or programatically, using +your iNaturalist credentials plus separate application credentials. These tokens are typically valid +for 24 hours, after which you will need to get a new one. + +You can then pass the access token to any API request function that uses it via the +`access_token` argument. For example: +```py +from pyinaturalist import create_observation + +create_observation( + ..., + access_token='my_access_token', +) +``` + +:::{note} Read-only requests generally don't require authentication; however, if you want to access +private data visible only to your user (for example, obscured or private coordinates), you will need +to use an access token. +::: + +## Manual Authentication +You can get an access token from a browser by logging in to inaturalist.org and going to +https://www.inaturalist.org/users/api_token. + +This has the advantage of not needing to create an iNaturalist application (see section below), but +has the disadvantage of requiring you to manually copy and paste the token. + +## Creating an Application +:::{dropdown} Why do I need to create an application? +:icon: info + +iNaturalist uses OAuth2, which provides several different methods (or "flows") to access the site. +For example, on the [login page](https://www.inaturalist.org/login), you have the option of logging +in with a username/password, or with an external provider (Google, Facebook, etc.): + +```{image} ../images/inat-user-login.png +:alt: Login form +:width: 150 +``` + +Outside of iNaturalist.org, anything else that uses the API to create or modify data is considered +an "application," even if you're just running some scripts on your own computer. + +See [iNaturalist documentation](https://www.inaturalist.org/pages/api+reference#auth) +for more details on authentication. +::: + +First, go to [New Application](https://www.inaturalist.org/oauth/applications/new) and fill out the +following pieces of information: + +* **Name:** Any name you want to come up with. For example, if this is associated with a GitHub repo, + you can use your repo name. +* **Description:** A brief description of what you'll be using this for. For example, + *"Data access for my own observations"*. +* **Confidential:** ✔️ This should be checked. +* **URL and Redirect URI:** Just enter the URL to your GitHub repo, if you have one; otherwise any + placeholder like "" will work. + +```{image} ../images/inat-new-application.png +:alt: New Application form +:width: 300 +``` + +You should then see a screen like this, which will show your new application ID and secret. These will +only be shown once, so save them somewhere secure, preferably in a password manager. +```{image} ../images/inat-new-application-complete.png +:alt: Completed application form +:width: 400 +``` + +## Basic Usage +There are a few different ways you can pass your credentials to iNaturalist. First, you can pass +them as keyword arguments to {py:func}`.get_access_token`: + +```python +>>> from pyinaturalist import get_access_token +>>> access_token = get_access_token( +>>> username='my_inaturalist_username', # Username you use to login to iNaturalist.org +>>> password='my_inaturalist_password', # Password you use to login to iNaturalist.org +>>> app_id='33f27dc63bdf27f4ca6cd95dd', # OAuth2 application ID +>>> app_secret='bbce628be722bfe2abde4', # OAuth2 application secret +>>> ) +``` + +## Environment Variables +You can also provide credentials via environment variables instead of arguments. The +environment variable names are the keyword arguments in uppercase, prefixed with `INAT_`: + +* `INAT_USERNAME` +* `INAT_PASSWORD` +* `INAT_APP_ID` +* `INAT_APP_SECRET` + +**Examples:** + +::::{tab-set} +:::{tab-item} Python +:sync: python + +```python +>>> import os +>>> os.environ['INAT_USERNAME'] = 'my_inaturalist_username' +>>> os.environ['INAT_PASSWORD'] = 'my_inaturalist_password' +>>> os.environ['INAT_APP_ID'] = '33f27dc63bdf27f4ca6cd95df' +>>> os.environ['INAT_APP_SECRET'] = 'bbce628be722bfe283de4' +``` +::: +:::{tab-item} Unix (MacOS / Linux) +:sync: unix + +```bash +export INAT_USERNAME="my_inaturalist_username" +export INAT_PASSWORD="my_inaturalist_password" +export INAT_APP_ID="33f27dc63bdf27f4ca6cd95df" +export INAT_APP_SECRET="bbce628be722bfe283de4" +``` +::: +:::{tab-item} Windows CMD +:sync: cmd + +```bat +set INAT_USERNAME="my_inaturalist_username" +set INAT_PASSWORD="my_inaturalist_password" +set INAT_APP_ID="33f27dc63bdf27f4ca6cd95df" +set INAT_APP_SECRET="bbce628be722bfe283de4" +``` +::: +:::{tab-item} PowerShell +:sync: ps1 + +```powershell +$Env:INAT_USERNAME="my_inaturalist_username" +$Env:INAT_PASSWORD="my_inaturalist_password" +$Env:INAT_APP_ID="33f27dc63bdf27f4ca6cd95df" +$Env:INAT_APP_SECRET="bbce628be722bfe283de4" +``` +::: +:::: + +Note that in any shell, these environment variables will only be set for your current shell +session. I.e., you can't set them in one terminal and then access them in another. + +## Keyring Integration +To handle your credentials more securely, you can store them in your system keyring. +You could manually store and retrieve them with a utility like +[secret-tool](https://manpages.ubuntu.com/manpages/xenial/man1/secret-tool.1.html) +and place them in environment variables as described above, but there is a much simpler option. + +Direct keyring integration is provided via [python keyring](https://github.com/jaraco/keyring). Most common keyring bakcends are supported, including: + +* macOS [Keychain](https://en.wikipedia.org/wiki/Keychain_%28software%29) +* Freedesktop [Secret Service](http://standards.freedesktop.org/secret-service/) +* KDE [KWallet](https://en.wikipedia.org/wiki/KWallet) +* [Windows Credential Locker](https://docs.microsoft.com/en-us/windows/uwp/security/credential-locker) + +To store your credentials in the keyring, run {py:func}`.set_keyring_credentials`: +```python +>>> from pyinaturalist.auth import set_keyring_credentials +>>> set_keyring_credentials( +>>> username='my_inaturalist_username', +>>> password='my_inaturalist_password', +>>> app_id='33f27dc63bdf27f4ca6cd95df', +>>> app_secret='bbce628be722bfe283de4', +>>> ) +``` + +Afterward, you can call {py:func}`.get_access_token` without any arguments, and your credentials +will be retrieved from the keyring. You do not need to run {py:func}`.set_keyring_credentials` +again unless you change your iNaturalist password. + +### Password Manager Integration +Keyring integration can be taken a step further by managing your keyring with a password +manager. This has the advantage of keeping your credentials in one place that can be synced +across multiple machines. [KeePassXC](https://keepassxc.org/) offers this feature for +macOS and Linux systems. See this guide for setup info: +[KeepassXC and secret service, a small walk-through](https://avaldes.co/2020/01/28/secret-service-keepassxc.html). + +```{figure} ../images/password_manager_keying.png +Credentials storage with keyring + KeePassXC +``` diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index d14b611e..c58ecd4c 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -5,6 +5,7 @@ :maxdepth: 2 general +authentication advanced client ``` diff --git a/examples/observation_photo_metadata.py b/examples/observation_photo_metadata.py index 73036205..3779615d 100644 --- a/examples/observation_photo_metadata.py +++ b/examples/observation_photo_metadata.py @@ -11,7 +11,7 @@ For more details on authentication, see: * https://www.inaturalist.org/pages/api+reference#auth -* https://pyinaturalist.readthedocs.io/en/stable/user_guide.html#authentication +* https://pyinaturalist.readthedocs.io/en/stable/authentication.md Extra dependencies: