Skip to content

Commit

Permalink
Merge pull request #300 from jrbourbeau/auto-login
Browse files Browse the repository at this point in the history
Support automatic authentication
  • Loading branch information
jrbourbeau authored Oct 16, 2023
2 parents 8483173 + 1cba817 commit d0386ae
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 27 deletions.
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,24 @@ The only requirement to use this library is to open a free account with NASA [ED

### **Authentication**

Once you have an EDL account, you can authenticate using one of the following three methods:
By default, `earthaccess` with automatically look for your EDL account credentials in two locations:

1. Using a `.netrc` file
* Can use *earthaccess* to read your EDL credentials (username and password) from a `.netrc` file
2. Reading your EDL credentials from environment variables
* if available you can use environment variables **EARTHDATA_USERNAME** and **EARTHDATA_PASSWORD**
3. Interactively entering your EDL credentials
* You can be prompted for these credentials and save them to a `.netrc` file
1. A `~/.netrc` file
2. `EARTHDATA_USERNAME` and `EARTHDATA_PASSWORD` environment variables

If neither of these options are configured, you can authenticate by calling the `earthaccess.login()` method
and manually entering your EDL account credentials.

```python
import earthaccess

auth = earthaccess.login()

earthaccess.login()
```

Note you can pass `persist=True` to `earthaccess.login()` to have the EDL account credentials you enter
automatically saved to a `~/.netrc` file for future use.


Once you are authenticated with NASA EDL you can:

* Get a file from a DAAC using a `fsspec` session.
Expand Down
45 changes: 42 additions & 3 deletions earthaccess/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import logging
import threading
from importlib.metadata import version
from typing import Any

Expand All @@ -19,6 +21,8 @@
from .search import DataCollections, DataGranules
from .store import Store

logger = logging.getLogger(__name__)

__all__ = [
"login",
"search_datasets",
Expand All @@ -38,7 +42,42 @@
"auth_environ",
]

__auth__ = Auth()
__store__: Any = None

__version__ = version("earthaccess")

_auth = Auth()
_store = None
_lock = threading.Lock()


def __getattr__(name): # type: ignore
"""
Module-level getattr to handle automatic authentication when accessing
`earthaccess.__auth__` and `earthaccess.__store__`.
Other unhandled attributes raise as `AttributeError` as expected.
"""
global _auth, _store

if name == "__auth__" or name == "__store__":
with _lock:
if not _auth.authenticated:
for strategy in ["environment", "netrc"]:
try:
_auth.login(strategy=strategy)
except Exception as e:
logger.debug(
f"An error occurred during automatic authentication with {strategy=}: {str(e)}"
)
continue
else:
if not _auth.authenticated:
continue
else:
_store = Store(_auth)
logger.debug(
f"Automatic authentication with {strategy=} was successful"
)
break
return _auth if name == "__auth__" else _store
else:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
33 changes: 18 additions & 15 deletions earthaccess/auth.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import getpass
import logging
import os
from netrc import NetrcParseError
from pathlib import Path
Expand All @@ -10,6 +11,8 @@

from .daac import DAACS

logger = logging.getLogger(__name__)


class SessionWithHeaderRedirection(requests.Session):
"""
Expand Down Expand Up @@ -76,7 +79,7 @@ def login(self, strategy: str = "netrc", persist: bool = False) -> Any:
an instance of Auth.
"""
if self.authenticated:
print("We are already authenticated with NASA EDL")
logger.debug("We are already authenticated with NASA EDL")
return self
if strategy == "interactive":
self._interactive(persist)
Expand All @@ -98,7 +101,7 @@ def refresh_tokens(self) -> bool:
if resp_tokens.ok:
self.token = resp_tokens.json()
self.tokens = [self.token]
print(
logger.debug(
f"earthaccess generated a token for CMR with expiration on: {self.token['expiration_date']}"
)
return True
Expand All @@ -111,7 +114,7 @@ def refresh_tokens(self) -> bool:
if resp_tokens.ok:
self.token = resp_tokens.json()
self.tokens.extend(self.token)
print(
logger.debug(
f"earthaccess generated a token for CMR with expiration on: {self.token['expiration_date']}"
)
return True
Expand All @@ -127,7 +130,7 @@ def refresh_tokens(self) -> bool:
if resp_tokens.ok:
self.token = resp_tokens.json()
self.tokens[0] = self.token
print(
logger.debug(
f"earthaccess generated a token for CMR with expiration on: {self.token['expiration_date']}"
)
return True
Expand Down Expand Up @@ -230,7 +233,7 @@ def _interactive(self, presist_credentials: bool = False) -> bool:
password = getpass.getpass(prompt="Enter your Earthdata password: ")
authenticated = self._get_credentials(username, password)
if authenticated:
print("Using user provided credentials for EDL")
logger.debug("Using user provided credentials for EDL")
if presist_credentials:
print("Persisting credentials to .netrc")
self._persist_user_credentials(username, password)
Expand All @@ -240,29 +243,29 @@ def _netrc(self) -> bool:
try:
my_netrc = Netrc()
except FileNotFoundError as err:
print(f"No .netrc found in {os.path.expanduser('~')}")
raise err
raise FileNotFoundError(
f"No .netrc found in {os.path.expanduser('~')}"
) from err
except NetrcParseError as err:
print("Unable to parse .netrc")
raise err
raise NetrcParseError("Unable to parse .netrc") from err
if my_netrc["urs.earthdata.nasa.gov"] is not None:
username = my_netrc["urs.earthdata.nasa.gov"]["login"]
password = my_netrc["urs.earthdata.nasa.gov"]["password"]
else:
return False
authenticated = self._get_credentials(username, password)
if authenticated:
print("Using .netrc file for EDL")
logger.debug("Using .netrc file for EDL")
return authenticated

def _environment(self) -> bool:
username = os.getenv("EARTHDATA_USERNAME")
password = os.getenv("EARTHDATA_PASSWORD")
authenticated = self._get_credentials(username, password)
if authenticated:
print("Using environment variables for EDL")
logger.debug("Using environment variables for EDL")
else:
print(
logger.debug(
"EARTHDATA_USERNAME and EARTHDATA_PASSWORD are not set in the current environment, try "
"setting them or use a different strategy (netrc, interactive)"
)
Expand All @@ -279,7 +282,7 @@ def _get_credentials(
f"Authentication with Earthdata Login failed with:\n{token_resp.text}"
)
return False
print("You're now authenticated with NASA Earthdata Login")
logger.debug("You're now authenticated with NASA Earthdata Login")
self.username = username
self.password = password

Expand All @@ -288,13 +291,13 @@ def _get_credentials(

if len(self.tokens) == 0:
self.refresh_tokens()
print(
logger.debug(
f"earthaccess generated a token for CMR with expiration on: {self.token['expiration_date']}"
)
self.token = self.tokens[0]
elif len(self.tokens) > 0:
self.token = self.tokens[0]
print(
logger.debug(
f"Using token with expiration date: {self.token['expiration_date']}"
)
profile = self.get_user_profile()
Expand Down

0 comments on commit d0386ae

Please sign in to comment.