Skip to content

Oauth2

Module dedicated to OAuth2 device flow.

DeviceGrant

Bases: GrantMethodBase

Device grant method.

Source code in teledetection/sdk/oauth2.py
class DeviceGrant(GrantMethodBase):
    """Device grant method."""

    client_id = "gdal"

    def get_first_token(self) -> JWT:
        """Get the first JWT token."""
        device_endpoint = f"{self.get_token_endpoint().rsplit('/', 1)[0]}/auth/device"

        req = create_session()
        log.debug("Getting token using device authorization grant")
        ret = req.post(
            device_endpoint,
            headers=self.headers,
            data=self.data_base,
            timeout=TIMEOUT,
        )
        if ret.status_code == 200:
            response = DeviceGrantResponse(**ret.json())
            verif_url_comp = response.verification_uri_complete
            log.info("Open the following URL in your browser to grant access:")
            log.info("\033[92m %s \033[0m", verif_url_comp)

            # QR code
            qr_code = qrcode.QRCode()
            qr_code.add_data(verif_url_comp)
            buffer = io.StringIO()
            qr_code.print_ascii(out=buffer)
            buffer.seek(0)
            log.info(buffer.read())

            log.info("Waiting for authentication...")
            start = time.time()
            data = self.data_base.copy()
            grant_type = "urn:ietf:params:oauth:grant-type:device_code"
            data.update(
                {
                    "device_code": response.device_code,
                    "grant_type": grant_type,
                }
            )
            while True:
                ret = req.post(
                    self.get_token_endpoint(),
                    headers=self.headers,
                    data=data,
                    timeout=TIMEOUT,
                )
                elapsed = time.time() - start
                log.debug(
                    "Elapsed: %.0f, status: %s (%.0f seconds left)",
                    elapsed,
                    ret.status_code,
                    response.expires_in - elapsed,
                )
                if elapsed > response.expires_in:
                    raise ExpiredAuthLinkError  # pragma: no cover
                if ret.status_code != 200:
                    time.sleep(response.interval)
                else:
                    return JWT.from_dict(ret.json())
        raise ConnectionError("Unable to authenticate with the SSO")

get_first_token()

Get the first JWT token.

Source code in teledetection/sdk/oauth2.py
def get_first_token(self) -> JWT:
    """Get the first JWT token."""
    device_endpoint = f"{self.get_token_endpoint().rsplit('/', 1)[0]}/auth/device"

    req = create_session()
    log.debug("Getting token using device authorization grant")
    ret = req.post(
        device_endpoint,
        headers=self.headers,
        data=self.data_base,
        timeout=TIMEOUT,
    )
    if ret.status_code == 200:
        response = DeviceGrantResponse(**ret.json())
        verif_url_comp = response.verification_uri_complete
        log.info("Open the following URL in your browser to grant access:")
        log.info("\033[92m %s \033[0m", verif_url_comp)

        # QR code
        qr_code = qrcode.QRCode()
        qr_code.add_data(verif_url_comp)
        buffer = io.StringIO()
        qr_code.print_ascii(out=buffer)
        buffer.seek(0)
        log.info(buffer.read())

        log.info("Waiting for authentication...")
        start = time.time()
        data = self.data_base.copy()
        grant_type = "urn:ietf:params:oauth:grant-type:device_code"
        data.update(
            {
                "device_code": response.device_code,
                "grant_type": grant_type,
            }
        )
        while True:
            ret = req.post(
                self.get_token_endpoint(),
                headers=self.headers,
                data=data,
                timeout=TIMEOUT,
            )
            elapsed = time.time() - start
            log.debug(
                "Elapsed: %.0f, status: %s (%.0f seconds left)",
                elapsed,
                ret.status_code,
                response.expires_in - elapsed,
            )
            if elapsed > response.expires_in:
                raise ExpiredAuthLinkError  # pragma: no cover
            if ret.status_code != 200:
                time.sleep(response.interval)
            else:
                return JWT.from_dict(ret.json())
    raise ConnectionError("Unable to authenticate with the SSO")

ExpiredAuthLinkError

Bases: Exception

Authentication link has expired.

Source code in teledetection/sdk/oauth2.py
class ExpiredAuthLinkError(Exception):
    """Authentication link has expired."""

GrantMethodBase

Base class for grant methods.

Source code in teledetection/sdk/oauth2.py
class GrantMethodBase:
    """Base class for grant methods."""

    client_id: str
    headers: Dict[str, str] = {"Content-Type": "application/x-www-form-urlencoded"}
    _token_endpoint: str | None = None

    def get_token_endpoint(self):
        """Get the token endpoint."""
        if not self._token_endpoint:
            self._token_endpoint = retrieve_token_endpoint()
        return retrieve_token_endpoint()

    @abstractmethod
    def get_first_token(self) -> JWT:
        """Provide the first used token."""
        raise NotImplementedError

    @property
    def data_base(self) -> Dict[str, str]:
        """Base payload."""
        return {
            "client_id": self.client_id,
            "scope": "openid offline_access",
        }

    def refresh_token(self, old_jwt: JWT) -> JWT:
        """Refresh the token."""
        log.debug("Refreshing token")
        assert old_jwt, "JWT is empty"
        data = self.data_base.copy()
        data.update(
            {
                "refresh_token": old_jwt.refresh_token,
                "grant_type": "refresh_token",
            }
        )
        ret = create_session().post(
            self.get_token_endpoint(),
            headers=self.headers,
            data=data,
            timeout=TIMEOUT,
        )
        if ret.status_code == 200:
            log.debug(ret.text)
            return JWT.from_dict(ret.json())
        raise RefreshTokenError

data_base property

Base payload.

get_first_token() abstractmethod

Provide the first used token.

Source code in teledetection/sdk/oauth2.py
@abstractmethod
def get_first_token(self) -> JWT:
    """Provide the first used token."""
    raise NotImplementedError

get_token_endpoint()

Get the token endpoint.

Source code in teledetection/sdk/oauth2.py
def get_token_endpoint(self):
    """Get the token endpoint."""
    if not self._token_endpoint:
        self._token_endpoint = retrieve_token_endpoint()
    return retrieve_token_endpoint()

refresh_token(old_jwt)

Refresh the token.

Source code in teledetection/sdk/oauth2.py
def refresh_token(self, old_jwt: JWT) -> JWT:
    """Refresh the token."""
    log.debug("Refreshing token")
    assert old_jwt, "JWT is empty"
    data = self.data_base.copy()
    data.update(
        {
            "refresh_token": old_jwt.refresh_token,
            "grant_type": "refresh_token",
        }
    )
    ret = create_session().post(
        self.get_token_endpoint(),
        headers=self.headers,
        data=data,
        timeout=TIMEOUT,
    )
    if ret.status_code == 200:
        log.debug(ret.text)
        return JWT.from_dict(ret.json())
    raise RefreshTokenError

OAuth2Session

Class to start an OAuth2 session.

Source code in teledetection/sdk/oauth2.py
class OAuth2Session:
    """Class to start an OAuth2 session."""

    def __init__(self, grant_type: type[GrantMethodBase] = DeviceGrant):
        """Initialize."""
        self.grant = grant_type()
        self.jwt_ttl_margin_seconds = 60
        self.jwt_issuance = datetime.datetime(year=1, month=1, day=1)
        self.jwt: JWT | None = None

    def save_token(self, now: datetime.datetime):
        """Save the JWT to disk."""
        if self.jwt:
            self.jwt_issuance = now
            self.jwt.to_config_dir()
        else:
            log.warning("No OAuth2 credentials to save")

    def _init_jwt(self):
        """Initialize the JWT."""
        if not self.jwt:
            # First JWT initialisation
            self.jwt = JWT.from_config_dir()
        if not self.jwt:
            # When JWT is still `None`, we use the grant method
            self.jwt = self.grant.get_first_token()
            self.save_token(datetime.datetime.now())

    def _refresh_if_needed(self):
        """Refresh the token if ttl is too short."""
        self._init_jwt()
        ttl_margin_seconds = 30
        now = datetime.datetime.now()
        jwt_expires_in = datetime.timedelta(seconds=self.jwt.expires_in)
        access_token_ttl = self.jwt_issuance + jwt_expires_in - now
        access_token_ttl_seconds = access_token_ttl.total_seconds()
        log.debug("access_token_ttl is %s", access_token_ttl_seconds)
        if access_token_ttl_seconds < ttl_margin_seconds:
            # Access token in not valid, but refresh might be
            try:
                self.jwt = self.grant.refresh_token(self.jwt)
            except RefreshTokenError as con_err:
                log.warning(  # pragma: no cover
                    "Unable to refresh token (reason: %s). "
                    "Renewing initial authentication.",
                    con_err,
                )
                self.jwt = self.grant.get_first_token()  # pragma: no cover
        else:
            # Token is still valid
            log.debug("Credentials still valid")
            return
        self.save_token(now)

    def get_access_token(self) -> str:
        """Return the access token."""
        self._init_jwt()
        self._refresh_if_needed()
        assert self.jwt
        return self.jwt.access_token

__init__(grant_type=DeviceGrant)

Initialize.

Source code in teledetection/sdk/oauth2.py
def __init__(self, grant_type: type[GrantMethodBase] = DeviceGrant):
    """Initialize."""
    self.grant = grant_type()
    self.jwt_ttl_margin_seconds = 60
    self.jwt_issuance = datetime.datetime(year=1, month=1, day=1)
    self.jwt: JWT | None = None

get_access_token()

Return the access token.

Source code in teledetection/sdk/oauth2.py
def get_access_token(self) -> str:
    """Return the access token."""
    self._init_jwt()
    self._refresh_if_needed()
    assert self.jwt
    return self.jwt.access_token

save_token(now)

Save the JWT to disk.

Source code in teledetection/sdk/oauth2.py
def save_token(self, now: datetime.datetime):
    """Save the JWT to disk."""
    if self.jwt:
        self.jwt_issuance = now
        self.jwt.to_config_dir()
    else:
        log.warning("No OAuth2 credentials to save")

RefreshTokenError

Bases: Exception

Unable to refresh token.

Source code in teledetection/sdk/oauth2.py
class RefreshTokenError(Exception):
    """Unable to refresh token."""

retrieve_token_endpoint()

Retrieve the token endpoint from the s3 signing endpoint.

Source code in teledetection/sdk/oauth2.py
def retrieve_token_endpoint():
    """Retrieve the token endpoint from the s3 signing endpoint."""
    openapi_url = urljoin(ENV.tld_signing_endpoint, "openapi.json")
    log.debug("Fetching OAuth2 endpoint from openapi url %s", openapi_url)
    _session = create_session()
    res = _session.get(openapi_url, timeout=TIMEOUT)
    res.raise_for_status()
    data = res.json()
    return data["components"]["securitySchemes"]["OAuth2PasswordBearer"]["flows"][
        "password"
    ]["tokenUrl"]