From f1819655dcf2c3bff5cbf5d71c410b029d62009f Mon Sep 17 00:00:00 2001 From: "Carl A. Adams" Date: Sat, 15 Mar 2025 17:05:17 -0700 Subject: [PATCH 1/9] WIP --- mkdocs.yml | 1 + src/planet_auth_utils/commands/cli/main.py | 2 ++ src/planet_auth_utils/commands/cli/oauth_cmd.py | 1 + src/planet_auth_utils/commands/cli/planet_legacy_auth_cmd.py | 1 + src/planet_auth_utils/commands/cli/util.py | 1 + 5 files changed, 6 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index bc0e378..771e145 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,6 +2,7 @@ site_name: Planet Auth Library site_description: Planet Auth Library site_url: https://planet.com/ strict: true +dev_addr: 127.0.0.1:8001 #watch: # - src diff --git a/src/planet_auth_utils/commands/cli/main.py b/src/planet_auth_utils/commands/cli/main.py index baf5b54..3554a7a 100644 --- a/src/planet_auth_utils/commands/cli/main.py +++ b/src/planet_auth_utils/commands/cli/main.py @@ -107,6 +107,7 @@ def cmd_plauth_version(): Show the version of planet auth components. """ print(f"planet-auth : {importlib.metadata.version('planet-auth')}") + # TODO: put config back. print(f"planet-auth : {importlib.metadata.version('planet-auth')}") @cmd_plauth.command("login") @@ -182,6 +183,7 @@ def cmd_plauth_login( ) print("Login succeeded.") # Errors should throw. + # TODO: Manage prompts post_login_cmd_helper( override_auth_context=override_auth_context, use_sops=sops, diff --git a/src/planet_auth_utils/commands/cli/oauth_cmd.py b/src/planet_auth_utils/commands/cli/oauth_cmd.py index cdcadf9..f62c706 100644 --- a/src/planet_auth_utils/commands/cli/oauth_cmd.py +++ b/src/planet_auth_utils/commands/cli/oauth_cmd.py @@ -184,6 +184,7 @@ def cmd_oauth_login( extra=extra, ) print("Login succeeded.") # Errors should throw. + # TODO: Manage prompts post_login_cmd_helper( override_auth_context=current_auth_context, use_sops=sops, diff --git a/src/planet_auth_utils/commands/cli/planet_legacy_auth_cmd.py b/src/planet_auth_utils/commands/cli/planet_legacy_auth_cmd.py index 59b265f..e31ab6b 100644 --- a/src/planet_auth_utils/commands/cli/planet_legacy_auth_cmd.py +++ b/src/planet_auth_utils/commands/cli/planet_legacy_auth_cmd.py @@ -64,6 +64,7 @@ def cmd_pllegacy_login(ctx, username, password, sops): password=password, ) print("Login succeeded.") # Errors should throw. + # TODO: Manage prompts post_login_cmd_helper( override_auth_context=current_auth_context, use_sops=sops, diff --git a/src/planet_auth_utils/commands/cli/util.py b/src/planet_auth_utils/commands/cli/util.py index 083349b..bc3fbd2 100644 --- a/src/planet_auth_utils/commands/cli/util.py +++ b/src/planet_auth_utils/commands/cli/util.py @@ -57,6 +57,7 @@ def post_login_cmd_helper(override_auth_context: planet_auth.Auth, use_sops): # If someone performed a login with a non-default profile, it's # reasonable to ask if they intend to change their defaults. + # TODO: provide a no-prompt option (or a -n/-y option?) Allow setting via planet.json prompt_change_user_default_profile_if_different(candidate_profile_name=override_profile_name) # If the config was created ad-hoc by the factory, the factory does From 3006036079b466706b7a7bcf153528c0fbd673c2 Mon Sep 17 00:00:00 2001 From: "Carl A. Adams" Date: Sat, 15 Mar 2025 20:18:48 -0700 Subject: [PATCH 2/9] enhance version command --- src/planet_auth_utils/commands/cli/main.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/planet_auth_utils/commands/cli/main.py b/src/planet_auth_utils/commands/cli/main.py index 3554a7a..56919bb 100644 --- a/src/planet_auth_utils/commands/cli/main.py +++ b/src/planet_auth_utils/commands/cli/main.py @@ -106,8 +106,15 @@ def cmd_plauth_version(): """ Show the version of planet auth components. """ - print(f"planet-auth : {importlib.metadata.version('planet-auth')}") - # TODO: put config back. print(f"planet-auth : {importlib.metadata.version('planet-auth')}") + def _pkg_display_version(pkg_name): + try: + return importlib.metadata.version(pkg_name) + except importlib.metadata.PackageNotFoundError: + return "N/A" + # Well known packages with built-in profile configs we commonly use. + print(f"planet-auth : {_pkg_display_version('planet-auth')}") + print(f"planet-auth-config : {_pkg_display_version('planet-auth-config')}") + print(f"planet : {_pkg_display_version('planet')}") @cmd_plauth.command("login") From 148a11406deff866d062f57b0277482b5576bb2b Mon Sep 17 00:00:00 2001 From: "Carl A. Adams" Date: Sat, 15 Mar 2025 21:11:19 -0700 Subject: [PATCH 3/9] skip user prompts with a CLI options. --- src/planet_auth_utils/__init__.py | 2 ++ src/planet_auth_utils/commands/cli/main.py | 11 ++++++----- src/planet_auth_utils/commands/cli/oauth_cmd.py | 9 ++++----- src/planet_auth_utils/commands/cli/options.py | 14 ++++++++++++++ .../commands/cli/planet_legacy_auth_cmd.py | 11 ++++------- src/planet_auth_utils/commands/cli/prompts.py | 7 +++++-- src/planet_auth_utils/commands/cli/util.py | 12 ++++++++---- 7 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/planet_auth_utils/__init__.py b/src/planet_auth_utils/__init__.py index 29ac18d..d2bee18 100644 --- a/src/planet_auth_utils/__init__.py +++ b/src/planet_auth_utils/__init__.py @@ -70,6 +70,7 @@ opt_sops, opt_token_file, opt_username, + opt_yes_no, ) from .commands.cli.util import recast_exceptions_to_click from planet_auth_utils.constants import EnvironmentVariables @@ -123,6 +124,7 @@ "opt_sops", "opt_token_file", "opt_username", + "opt_yes_no", "recast_exceptions_to_click", # "Builtins", diff --git a/src/planet_auth_utils/commands/cli/main.py b/src/planet_auth_utils/commands/cli/main.py index 56919bb..c3c9633 100644 --- a/src/planet_auth_utils/commands/cli/main.py +++ b/src/planet_auth_utils/commands/cli/main.py @@ -37,6 +37,7 @@ opt_audience, opt_token_file, opt_scope, + opt_yes_no, ) from .oauth_cmd import cmd_oauth from .planet_legacy_auth_cmd import cmd_pllegacy @@ -106,11 +107,13 @@ def cmd_plauth_version(): """ Show the version of planet auth components. """ + def _pkg_display_version(pkg_name): try: return importlib.metadata.version(pkg_name) except importlib.metadata.PackageNotFoundError: return "N/A" + # Well known packages with built-in profile configs we commonly use. print(f"planet-auth : {_pkg_display_version('planet-auth')}") print(f"planet-auth-config : {_pkg_display_version('planet-auth-config')}") @@ -131,6 +134,7 @@ def _pkg_display_version(pkg_name): @opt_username() @opt_password() @opt_sops +@opt_yes_no @click.pass_context @recast_exceptions_to_click(AuthException, FileNotFoundError, PermissionError) def cmd_plauth_login( @@ -148,6 +152,7 @@ def cmd_plauth_login( username, password, sops, + yes, ): """ Perform an initial login, obtain user authorization, and save access @@ -190,11 +195,7 @@ def cmd_plauth_login( ) print("Login succeeded.") # Errors should throw. - # TODO: Manage prompts - post_login_cmd_helper( - override_auth_context=override_auth_context, - use_sops=sops, - ) + post_login_cmd_helper(override_auth_context=override_auth_context, use_sops=sops, prompt_pre_selection=yes) cmd_plauth.add_command(cmd_oauth) diff --git a/src/planet_auth_utils/commands/cli/oauth_cmd.py b/src/planet_auth_utils/commands/cli/oauth_cmd.py index f62c706..1516c8a 100644 --- a/src/planet_auth_utils/commands/cli/oauth_cmd.py +++ b/src/planet_auth_utils/commands/cli/oauth_cmd.py @@ -42,6 +42,7 @@ opt_show_qr_code, opt_sops, opt_username, + opt_yes_no, ) from .util import recast_exceptions_to_click, post_login_cmd_helper, print_obj @@ -142,6 +143,7 @@ def cmd_oauth(ctx): @opt_client_id @opt_client_secret @opt_sops +@opt_yes_no @click.pass_context @recast_exceptions_to_click(AuthException) def cmd_oauth_login( @@ -156,6 +158,7 @@ def cmd_oauth_login( auth_client_id, auth_client_secret, sops, + yes, project, ): """ @@ -184,11 +187,7 @@ def cmd_oauth_login( extra=extra, ) print("Login succeeded.") # Errors should throw. - # TODO: Manage prompts - post_login_cmd_helper( - override_auth_context=current_auth_context, - use_sops=sops, - ) + post_login_cmd_helper(override_auth_context=current_auth_context, use_sops=sops, prompt_pre_selection=yes) @cmd_oauth.command("refresh") diff --git a/src/planet_auth_utils/commands/cli/options.py b/src/planet_auth_utils/commands/cli/options.py index 56b1af3..460663b 100644 --- a/src/planet_auth_utils/commands/cli/options.py +++ b/src/planet_auth_utils/commands/cli/options.py @@ -191,6 +191,20 @@ def opt_loglevel(function): return function +def opt_yes_no(function): + """ + Click option to bypass prompts with a yes or no selection. + """ + function = click.option( + "--yes/--no", + "-y/-n", + help='Skip user prompts with a "yes" or "no" selection', + default=None, + show_default=True, + )(function) + return function + + def opt_human_readable(function): """ Click option to toggle raw / human-readable formatting. diff --git a/src/planet_auth_utils/commands/cli/planet_legacy_auth_cmd.py b/src/planet_auth_utils/commands/cli/planet_legacy_auth_cmd.py index e31ab6b..acb4a0e 100644 --- a/src/planet_auth_utils/commands/cli/planet_legacy_auth_cmd.py +++ b/src/planet_auth_utils/commands/cli/planet_legacy_auth_cmd.py @@ -22,7 +22,7 @@ PlanetLegacyAuthClientConfig, ) -from .options import opt_password, opt_sops, opt_username +from .options import opt_password, opt_sops, opt_username, opt_yes_no from .util import recast_exceptions_to_click, post_login_cmd_helper @@ -51,8 +51,9 @@ def cmd_pllegacy(ctx): @opt_password(hidden=False) @opt_username(hidden=False) @opt_sops +@opt_yes_no @click.pass_context -def cmd_pllegacy_login(ctx, username, password, sops): +def cmd_pllegacy_login(ctx, username, password, sops, yes): """ Perform an initial login using Planet's legacy authentication interfaces. """ @@ -64,11 +65,7 @@ def cmd_pllegacy_login(ctx, username, password, sops): password=password, ) print("Login succeeded.") # Errors should throw. - # TODO: Manage prompts - post_login_cmd_helper( - override_auth_context=current_auth_context, - use_sops=sops, - ) + post_login_cmd_helper(override_auth_context=current_auth_context, use_sops=sops, prompt_pre_selection=yes) @cmd_pllegacy.command("print-api-key") diff --git a/src/planet_auth_utils/commands/cli/prompts.py b/src/planet_auth_utils/commands/cli/prompts.py index 68db18a..7deba9d 100644 --- a/src/planet_auth_utils/commands/cli/prompts.py +++ b/src/planet_auth_utils/commands/cli/prompts.py @@ -17,6 +17,7 @@ # and click or simple code for others. import click +from typing import Optional # from prompt_toolkit.shortcuts import input_dialog, radiolist_dialog, yes_no_dialog @@ -25,8 +26,8 @@ from planet_auth_utils.constants import EnvironmentVariables -def prompt_change_user_default_profile_if_different( - candidate_profile_name: str, +def prompt_and_change_user_default_profile_if_different( + candidate_profile_name: str, change_default_selection: Optional[bool] = None ): config_file = PlanetAuthUserConfigEnhanced() try: @@ -39,6 +40,8 @@ def prompt_change_user_default_profile_if_different( # Since CLI options and env vars are higher priority than this file, # it should not cause surprises. do_change_default = True + elif change_default_selection is not None: + do_change_default = change_default_selection else: do_change_default = False if saved_profile_name != candidate_profile_name: diff --git a/src/planet_auth_utils/commands/cli/util.py b/src/planet_auth_utils/commands/cli/util.py index bc3fbd2..929714d 100644 --- a/src/planet_auth_utils/commands/cli/util.py +++ b/src/planet_auth_utils/commands/cli/util.py @@ -15,6 +15,7 @@ import click import functools import json +from typing import Optional import planet_auth from planet_auth.constants import AUTH_CONFIG_FILE_SOPS, AUTH_CONFIG_FILE_PLAIN @@ -22,7 +23,7 @@ from planet_auth_utils.builtins import Builtins from planet_auth_utils.profile import Profile -from .prompts import prompt_change_user_default_profile_if_different +from .prompts import prompt_and_change_user_default_profile_if_different def recast_exceptions_to_click(*exceptions, **params): # pylint: disable=W0613 @@ -48,7 +49,9 @@ def print_obj(obj): print(json_str) -def post_login_cmd_helper(override_auth_context: planet_auth.Auth, use_sops): +def post_login_cmd_helper( + override_auth_context: planet_auth.Auth, use_sops, prompt_pre_selection: Optional[bool] = None +): override_profile_name = override_auth_context.profile_name() if not override_profile_name: # Can't save to a profile if there is none. We don't really expect this in the cases @@ -57,8 +60,9 @@ def post_login_cmd_helper(override_auth_context: planet_auth.Auth, use_sops): # If someone performed a login with a non-default profile, it's # reasonable to ask if they intend to change their defaults. - # TODO: provide a no-prompt option (or a -n/-y option?) Allow setting via planet.json - prompt_change_user_default_profile_if_different(candidate_profile_name=override_profile_name) + prompt_and_change_user_default_profile_if_different( + candidate_profile_name=override_profile_name, change_default_selection=prompt_pre_selection + ) # If the config was created ad-hoc by the factory, the factory does # not associate it with a file to support factory use in a context From 9f895d776cb251c4eadcfae6db29c21ec7973c0b Mon Sep 17 00:00:00 2001 From: "Carl A. Adams" Date: Mon, 17 Mar 2025 08:14:58 -0700 Subject: [PATCH 4/9] docs --- docs/changelog.md | 2 +- docs/index.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index c8cb442..b3df47e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 2.0.4.X - 2025-03-09 +## 2.0.4.X - 2025 - Initial Beta release series to shakedown public release pipelines and initial integrations. diff --git a/docs/index.md b/docs/index.md index 00eef2b..2093494 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,15 +1,15 @@ # Planet Auth Utility Library The Planet Auth Library provides generic authentication utilities for clients -and for services. For clients, it provides means to obtain access tokens that +and services. For clients, it provides the means to obtain access tokens that can be used to access network services. For services, it provides tools to validate the same access tokens. The architecture of the code was driven by OAuth2, but is intended to be easily -extensible to new authentication protocols in the future. Since both clients +extensible to new authentication protocols in the future. Since clients and resource servers are both themselves clients to authorization servers in -an OAuth2 deployment, this combining of resource client and resource server -concerns in a single library was seen as natural. +an OAuth2 deployment, this combining of client and server concerns in a single +library was seen as natural. Currently, this library supports OAuth2, Planet's legacy proprietary authentication protocols, and static API keys. From 50dfcb9b7797abcddc392ef1abdeae4349dd5f06 Mon Sep 17 00:00:00 2001 From: "Carl A. Adams" Date: Tue, 18 Mar 2025 19:38:20 -0700 Subject: [PATCH 5/9] JWT util command --- src/planet_auth/oidc/token_validator.py | 2 +- src/planet_auth_utils/__init__.py | 12 + src/planet_auth_utils/commands/cli/jwt_cmd.py | 253 ++++++++++++++++++ src/planet_auth_utils/commands/cli/main.py | 8 +- .../commands/cli/oauth_cmd.py | 83 +----- src/planet_auth_utils/commands/cli/options.py | 48 +++- src/planet_auth_utils/constants.py | 14 +- src/planet_auth_utils/plauth_factory.py | 2 +- 8 files changed, 335 insertions(+), 87 deletions(-) create mode 100644 src/planet_auth_utils/commands/cli/jwt_cmd.py diff --git a/src/planet_auth/oidc/token_validator.py b/src/planet_auth/oidc/token_validator.py index ea57822..9c31a13 100644 --- a/src/planet_auth/oidc/token_validator.py +++ b/src/planet_auth/oidc/token_validator.py @@ -257,7 +257,7 @@ def validate_token( return validated_claims @staticmethod - def unverified_decode(token_str): + def hazmat_unverified_decode(token_str): # WARNING: Treat unverified token claims like toxic waste. # Nothing can be trusted until the token is verified. unverified_complete = jwt.decode_complete(token_str, options={"verify_signature": False}) # nosemgrep diff --git a/src/planet_auth_utils/__init__.py b/src/planet_auth_utils/__init__.py index d2bee18..4957595 100644 --- a/src/planet_auth_utils/__init__.py +++ b/src/planet_auth_utils/__init__.py @@ -51,12 +51,18 @@ cmd_profile_set, cmd_profile_show, ) +from .commands.cli.jwt_cmd import ( + cmd_jwt, + cmd_jwt_decode, + cmd_jwt_validate_oauth, +) from .commands.cli.options import ( opt_api_key, opt_audience, opt_client_id, opt_client_secret, opt_human_readable, + opt_issuer, opt_loglevel, opt_long, opt_open_browser, @@ -68,6 +74,7 @@ opt_scope, opt_show_qr_code, opt_sops, + opt_token, opt_token_file, opt_username, opt_yes_no, @@ -82,6 +89,9 @@ __all__ = [ "cmd_plauth_embedded", "cmd_plauth_login", + "cmd_jwt", + "cmd_jwt_decode", + "cmd_jwt_validate_oauth", "cmd_oauth", "cmd_oauth_login", "cmd_oauth_refresh", @@ -111,6 +121,7 @@ "opt_client_id", "opt_client_secret", "opt_human_readable", + "opt_issuer", "opt_loglevel", "opt_long", "opt_open_browser", @@ -122,6 +133,7 @@ "opt_scope", "opt_show_qr_code", "opt_sops", + "opt_token", "opt_token_file", "opt_username", "opt_yes_no", diff --git a/src/planet_auth_utils/commands/cli/jwt_cmd.py b/src/planet_auth_utils/commands/cli/jwt_cmd.py new file mode 100644 index 0000000..7c7e770 --- /dev/null +++ b/src/planet_auth_utils/commands/cli/jwt_cmd.py @@ -0,0 +1,253 @@ +# Copyright 2025 Planet Labs PBC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +import json +import pathlib +import sys +import textwrap +import time +import typing + +from planet_auth import ( + AuthException, + TokenValidator, + OidcMultiIssuerValidator, +) +from planet_auth.util import custom_json_class_dumper + +from .options import ( + opt_audience, + opt_issuer, + opt_token, + opt_token_file, + opt_human_readable, +) +from .util import recast_exceptions_to_click, post_login_cmd_helper, print_obj + + +class _jwt_human_dumps: + """ + Wrapper object for controlling the json.dumps behavior of JWTs so that + we can display a version different from what is stored in memory. + + For pretty printing JWTs, we convert timestamps into + human-readable strings. + """ + + def __init__(self, data): + self._data = data + + def __json_pretty_dumps__(self): + def _human_timestamp_iso(d): + for key, value in list(d.items()): + if key in ["iat", "exp", "nbf"] and isinstance(value, int): + fmt_time = time.strftime("%Y-%m-%dT%H:%M:%S%z", time.localtime(value)) + if (key == "exp") and (d[key] < time.time()): + fmt_time += " (Expired)" + d[key] = fmt_time + elif isinstance(value, dict): + _human_timestamp_iso(value) + return d + + json_dumps = self._data.copy() + _human_timestamp_iso(json_dumps) + return json_dumps + + +def json_dumps_for_jwt_dict(data: dict, human_readable: bool, indent: int = 2): + if human_readable: + return json.dumps(_jwt_human_dumps(data), indent=indent, sort_keys=True, default=custom_json_class_dumper) + else: + return json.dumps(data, indent=2, sort_keys=True) + + +def print_jwt_parts(raw, header, body, signature, human_readable): + if raw: + print(f"RAW:\n {raw}\n") + + if header: + print( + f'HEADER:\n{textwrap.indent(json_dumps_for_jwt_dict(data=header, human_readable=human_readable), prefix=" ")}\n' + ) + + if body: + print( + f'BODY:\n{textwrap.indent(json_dumps_for_jwt_dict(body, human_readable=human_readable), prefix=" ")}\n' + ) + + if signature: + pretty_hex_signature = "" + i = 0 + for c in signature: + if i == 0: + pass + elif (i % 16) != 0: + pretty_hex_signature += ":" + else: + pretty_hex_signature += "\n" + + pretty_hex_signature += "{:02x}".format(c) + i += 1 + + print(f'SIGNATURE:\n{textwrap.indent(pretty_hex_signature, prefix=" ")}\n') + + +def hazmat_print_jwt(token_str, human_readable): + print("UNTRUSTED JWT Decoding\n") + if token_str: + (hazmat_header, hazmat_body, hazmat_signature) = TokenValidator.hazmat_unverified_decode(token_str) + print_jwt_parts( + raw=token_str, + header=hazmat_header, + body=hazmat_body, + signature=hazmat_signature, + human_readable=human_readable, + ) + + +@click.group("jwt", invoke_without_command=True) +@click.pass_context +def cmd_jwt(ctx): + """ + JWT utility for working with tokens. These functions are primarily targeted + towards debugging usage. Many of the functions do not perform token validation. + THE CONTENTS OF UNVALIDATED TOKENS MUST BE TREATED AS UNTRUSTED AND POTENTIALLY + MALICIOUS. + """ + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + sys.exit(0) + + +def _get_token_or_fail(token_opt: typing.Optional[str], token_file_opt: typing.Optional[pathlib.Path]): + if token_opt: + token = token_opt + elif token_file_opt: + with open(token_file_opt, mode="r", encoding="UTF-8") as file_r: + token = file_r.read() + else: + # click.echo(ctx.get_help()) + # click.echo() + raise click.UsageError("A token must be provided.") + return token + + +@cmd_jwt.command("decode") +@click.pass_context +@opt_human_readable +@opt_token +@opt_token_file +@recast_exceptions_to_click(AuthException, FileNotFoundError) +def cmd_jwt_decode(ctx, token: str, token_file: pathlib.Path, human_readable): + """ + Decode a JWT token WITHOUT PERFORMING ANY VALIDATION. + """ + token_to_print = _get_token_or_fail(token_opt=token, token_file_opt=token_file) + hazmat_print_jwt(token_str=token_to_print, human_readable=human_readable) + + +@cmd_jwt.command("validate-oauth") +@click.pass_context +@opt_human_readable +@opt_token +@opt_token_file +@opt_audience() +@opt_issuer() +@recast_exceptions_to_click(AuthException, FileNotFoundError) +def cmd_jwt_validate_oauth(ctx, token, token_file, audience, issuer, human_readable): + """ + Perform signature validation on an RFC 9068 compliant JWT token. + The `iss` and `aud` claims will be used to look up signing keys + using OAuth2/OIDC discovery protocols and perform basic validation + checks. + + This command performs only basic signature verification and token validity + checks. For checks against auth server token revocation lists, see the `oauth` + command. For deeper checks specific to the claims and structure of + Identity or Access tokens, see the `oauth` command. + + WARNING:\n + THIS TOOL IS ABSOLUTELY INAPPROPRIATE FOR PRODUCTION TRUST USAGE. This is a + development and debugging utility. The default behavior to inspect the token + for issuer and audience information used to validate the token is wholly + incorrect for a production use case. The decision of which issuers to + trust with which audiences MUST be controlled by the service operator. + """ + token_to_validate = _get_token_or_fail(token_opt=token, token_file_opt=token_file) + (hazmat_header, hazmat_body, hazmat_signature) = TokenValidator.hazmat_unverified_decode(token_to_validate) + + if issuer: + validation_iss = issuer + else: + if not hazmat_body.get("iss"): + raise click.BadParameter( + "The provided token does not contain an `iss` claim." " Is the provided JWT RFC 9068 compliant?" + ) + validation_iss = hazmat_body.get("iss") + + if audience: + validation_aud = audience + else: + if not hazmat_body.get("aud"): + raise click.BadParameter( + "The provided token does not contain an `aud` claim." " Is the provided JWT RFC 9068 compliant?" + ) + hazmat_aud = hazmat_body.get("aud") + if isinstance(hazmat_aud, list): + validation_aud = hazmat_aud[0] + else: + validation_aud = hazmat_aud + + validator = OidcMultiIssuerValidator.from_auth_server_urls( + trusted_auth_server_urls=[validation_iss], audience=validation_aud, log_result=False + ) + validated_body, _ = validator.validate_access_token(token_to_validate, do_remote_revocation_check=False) + # Validation throws on error + click.echo("TOKEN OK") + print_jwt_parts( + raw=token_to_validate, + header=hazmat_header, + body=validated_body, + signature=hazmat_signature, + human_readable=human_readable, + ) + + +@cmd_jwt.command("validate-rs256") +@click.pass_context +@opt_human_readable +@opt_token +@opt_token_file +@recast_exceptions_to_click(AuthException, FileNotFoundError, NotImplementedError) +def cmd_jwt_validate_rs256(ctx, token, token_file, human_readable): + """ + Validate a JWT signed with a RS256 signature + """ + # token_to_validate = _get_token_or_fail(token_opt=token, token_file_opt=token_file) + raise NotImplementedError("Command not implemented") + + +@cmd_jwt.command("validate-hs512") +@click.pass_context +@opt_human_readable +@opt_token +@opt_token_file +@recast_exceptions_to_click(AuthException, FileNotFoundError, NotImplementedError) +def cmd_jwt_validate_hs512(ctx, token, token_file, human_readable): + """ + Validate a JWT signed with a HS512 signature + """ + # token_to_validate = _get_token_or_fail(token_opt=token, token_file_opt=token_file) + raise NotImplementedError("Command not implemented") diff --git a/src/planet_auth_utils/commands/cli/main.py b/src/planet_auth_utils/commands/cli/main.py index c3c9633..6da6cb1 100644 --- a/src/planet_auth_utils/commands/cli/main.py +++ b/src/planet_auth_utils/commands/cli/main.py @@ -42,16 +42,16 @@ from .oauth_cmd import cmd_oauth from .planet_legacy_auth_cmd import cmd_pllegacy from .profile_cmd import cmd_profile +from .jwt_cmd import cmd_jwt from .util import recast_exceptions_to_click, post_login_cmd_helper @click.group("plauth", invoke_without_command=True, help="Planet authentication utility") @opt_loglevel @opt_profile -@opt_token_file # Remove? The interactions with changing the profile in login are not great. @click.pass_context @recast_exceptions_to_click(AuthException, FileNotFoundError, PermissionError) -def cmd_plauth(ctx, loglevel, auth_profile, token_file): +def cmd_plauth(ctx, loglevel, auth_profile): """ Planet Auth Utility commands """ @@ -68,7 +68,7 @@ def cmd_plauth(ctx, loglevel, auth_profile, token_file): ctx.obj["AUTH"] = PlanetAuthFactory.initialize_auth_client_context( auth_profile_opt=auth_profile, - token_file_opt=token_file, + # token_file_opt=token_file, ) @@ -201,10 +201,12 @@ def cmd_plauth_login( cmd_plauth.add_command(cmd_oauth) cmd_plauth.add_command(cmd_pllegacy) cmd_plauth.add_command(cmd_profile) +cmd_plauth.add_command(cmd_jwt) cmd_plauth_embedded.add_command(cmd_oauth) cmd_plauth_embedded.add_command(cmd_pllegacy) cmd_plauth_embedded.add_command(cmd_profile) +cmd_plauth_embedded.add_command(cmd_jwt) cmd_plauth_embedded.add_command(cmd_plauth_login) cmd_plauth_embedded.add_command(cmd_plauth_version) diff --git a/src/planet_auth_utils/commands/cli/oauth_cmd.py b/src/planet_auth_utils/commands/cli/oauth_cmd.py index 1516c8a..3b4f56e 100644 --- a/src/planet_auth_utils/commands/cli/oauth_cmd.py +++ b/src/planet_auth_utils/commands/cli/oauth_cmd.py @@ -13,10 +13,7 @@ # limitations under the License. import click -import json import sys -import textwrap -import time from planet_auth import ( AuthException, @@ -45,69 +42,7 @@ opt_yes_no, ) from .util import recast_exceptions_to_click, post_login_cmd_helper, print_obj - - -class _jwt_human_dumps: - """ - Wrapper object for controlling the json.dumps behavior of JWTs so that - we can display a version different from what is stored in memory. - - For pretty printing JWTs, we convert timestamps into - human-readable strings. - """ - - def __init__(self, data): - self._data = data - - def __json_pretty_dumps__(self): - def _human_timestamp_iso(d): - for key, value in list(d.items()): - if key in ["iat", "exp", "nbf"] and isinstance(value, int): - fmt_time = time.strftime("%Y-%m-%dT%H:%M:%S%z", time.localtime(value)) - if (key == "exp") and (d[key] < time.time()): - fmt_time += " (Expired)" - d[key] = fmt_time - elif isinstance(value, dict): - _human_timestamp_iso(value) - return d - - json_dumps = self._data.copy() - _human_timestamp_iso(json_dumps) - return json_dumps - - -def _json_dumps_for_jwt_dict(data: dict, human_readable: bool): - if human_readable: - return json.dumps(_jwt_human_dumps(data), indent=2, sort_keys=True, default=custom_json_class_dumper) - else: - return json.dumps(data, indent=2, sort_keys=True) - - -def _print_jwt(token_str, human_readable): - print("Untrusted JWT Decoding\n") - print(f"RAW:\n {token_str}\n") - if token_str: - (header, body, signature) = TokenValidator.unverified_decode(token_str) - pretty_hex_signature = "" - i = 0 - for c in signature: - if i == 0: - pass - elif (i % 16) != 0: - pretty_hex_signature += ":" - else: - pretty_hex_signature += "\n" - - pretty_hex_signature += "{:02x}".format(c) - i += 1 - - print( - f'HEADER:\n{textwrap.indent(_json_dumps_for_jwt_dict(data=header, human_readable=human_readable), prefix=" ")}\n' - ) - print( - f'BODY:\n{textwrap.indent(_json_dumps_for_jwt_dict(body, human_readable=human_readable), prefix=" ")}\n' - ) - print(f'SIGNATURE:\n{textwrap.indent(pretty_hex_signature, prefix=" ")}\n') +from .jwt_cmd import json_dumps_for_jwt_dict, hazmat_print_jwt def _check_client_type(ctx): @@ -248,7 +183,7 @@ def cmd_oauth_validate_access_token_remote(ctx, human_readable): print_obj("INVALID") sys.exit(1) # print_obj(validation_json) - print(_json_dumps_for_jwt_dict(data=validation_json, human_readable=human_readable)) + print(json_dumps_for_jwt_dict(data=validation_json, human_readable=human_readable)) @cmd_oauth.command("validate-access-token-local") @@ -281,7 +216,7 @@ def cmd_oauth_validate_access_token_local(ctx, audience, scope, human_readable): access_token=saved_token.access_token(), required_audience=audience, scopes_anyof=scope ) # print_obj(validation_json) - print(_json_dumps_for_jwt_dict(data=validation_json, human_readable=human_readable)) + print(json_dumps_for_jwt_dict(data=validation_json, human_readable=human_readable)) @cmd_oauth.command("validate-id-token") @@ -302,7 +237,7 @@ def cmd_oauth_validate_id_token_remote(ctx, human_readable): print_obj("INVALID") sys.exit(1) # print_obj(validation_json) - print(_json_dumps_for_jwt_dict(data=validation_json, human_readable=human_readable)) + print(json_dumps_for_jwt_dict(data=validation_json, human_readable=human_readable)) @cmd_oauth.command("validate-id-token-local") @@ -322,7 +257,7 @@ def cmd_oauth_validate_id_token_local(ctx, human_readable): # Throws on error. validation_json = auth_client.validate_id_token_local(saved_token.id_token()) # print_obj(validation_json) - print(_json_dumps_for_jwt_dict(data=validation_json, human_readable=human_readable)) + print(json_dumps_for_jwt_dict(data=validation_json, human_readable=human_readable)) @cmd_oauth.command("validate-refresh-token") @@ -343,7 +278,7 @@ def cmd_oauth_validate_refresh_token_remote(ctx, human_readable): print_obj("INVALID") sys.exit(1) # print_obj(validation_json) - print(_json_dumps_for_jwt_dict(data=validation_json, human_readable=human_readable)) + print(json_dumps_for_jwt_dict(data=validation_json, human_readable=human_readable)) @cmd_oauth.command("revoke-access-token") @@ -448,7 +383,7 @@ def cmd_oauth_decode_jwt_access_token(ctx, human_readable): access tokens in other formats. """ saved_token = FileBackedOidcCredential(None, ctx.obj["AUTH"].token_file_path()) - _print_jwt(saved_token.access_token(), human_readable=human_readable) + hazmat_print_jwt(saved_token.access_token(), human_readable=human_readable) @cmd_oauth.command("decode-id-token") @@ -462,7 +397,7 @@ def cmd_oauth_decode_jwt_id_token(ctx, human_readable): debugging purposes. """ saved_token = FileBackedOidcCredential(None, ctx.obj["AUTH"].token_file_path()) - _print_jwt(saved_token.id_token(), human_readable=human_readable) + hazmat_print_jwt(saved_token.id_token(), human_readable=human_readable) @cmd_oauth.command("decode-refresh-token") @@ -478,4 +413,4 @@ def cmd_oauth_decode_jwt_refresh_token(ctx, human_readable): refresh tokens in other formats. """ saved_token = FileBackedOidcCredential(None, ctx.obj["AUTH"].token_file_path()) - _print_jwt(saved_token.refresh_token(), human_readable=human_readable) + hazmat_print_jwt(saved_token.refresh_token(), human_readable=human_readable) diff --git a/src/planet_auth_utils/commands/cli/options.py b/src/planet_auth_utils/commands/cli/options.py index 460663b..f90c28e 100644 --- a/src/planet_auth_utils/commands/cli/options.py +++ b/src/planet_auth_utils/commands/cli/options.py @@ -13,6 +13,7 @@ # limitations under the License. import click +import pathlib from planet_auth_utils.constants import EnvironmentVariables @@ -261,6 +262,21 @@ def opt_show_qr_code(function): return function +def opt_token(function): + """ + Click option for specifying a token literal. + """ + function = click.option( + "--token", + help="Token string.", + type=str, + # envvar=EnvironmentVariables.AUTH_TOKEN, + show_envvar=False, + show_default=False, + )(function) + return function + + def opt_token_file(function): """ Click option for specifying a token file location for the @@ -268,16 +284,37 @@ def opt_token_file(function): """ function = click.option( "--token-file", - type=click.Path(), + type=click.Path(exists=True, file_okay=True, readable=True, path_type=pathlib.Path), envvar=EnvironmentVariables.AUTH_TOKEN_FILE, - help="Auth token file. The default will be to use a location in the profile directory ~/.planet/", + help="File containing a token.", default=None, - show_envvar=True, + show_envvar=False, show_default=True, )(function) return function +def opt_issuer(required=False): + def decorator(function): + """ + Click option for specifying an OAuth token issuer for the + planet_auth package's click commands. + """ + function = click.option( + "--issuer", + type=str, + envvar=EnvironmentVariables.AUTH_ISSUER, + help="Token issuer.", + default=None, + show_envvar=False, + show_default=False, + required=required, + )(function) + return function + + return decorator + + def opt_audience(required=False): def decorator(function): """ @@ -289,10 +326,9 @@ def decorator(function): multiple=True, type=str, envvar=EnvironmentVariables.AUTH_AUDIENCE, - help="Token audiences to request. Specify multiple options to request" + help="Token audiences. Specify multiple options to set" " multiple audiences. When set via environment variable, audiences" - " should be white space delimited. Default value is determined" - " by the selected auth profile.", + " should be white space delimited.", default=None, show_envvar=True, show_default=True, diff --git a/src/planet_auth_utils/constants.py b/src/planet_auth_utils/constants.py index 53bc571..a2a6648 100644 --- a/src/planet_auth_utils/constants.py +++ b/src/planet_auth_utils/constants.py @@ -38,14 +38,24 @@ class EnvironmentVariables: Name of a profile to use for auth client configuration. """ + AUTH_TOKEN = "PL_AUTH_TOKEN" + """ + Literal token string. + """ + AUTH_TOKEN_FILE = "PL_AUTH_TOKEN_FILE" """ - File path to use for storing OAuth tokens. + File path to use for storing tokens. + """ + + AUTH_ISSUER = "PL_AUTH_ISSUER" + """ + Issuer to use when requesting or validating OAuth tokens. """ AUTH_AUDIENCE = "PL_AUTH_AUDIENCE" """ - Audience to use when requesting OAuth tokens. + Audience to use when requesting or validating OAuth tokens. """ AUTH_ORGANIZATION = "PL_AUTH_ORGANIZATION" diff --git a/src/planet_auth_utils/plauth_factory.py b/src/planet_auth_utils/plauth_factory.py index 29a3cf1..9085c51 100644 --- a/src/planet_auth_utils/plauth_factory.py +++ b/src/planet_auth_utils/plauth_factory.py @@ -235,7 +235,7 @@ def initialize_auth_client_context( auth_client_id_opt: Optional[str] = None, auth_client_secret_opt: Optional[str] = None, auth_api_key_opt: Optional[str] = None, # Deprecated - token_file_opt: Optional[str] = None, # TODO: Remove, but we still depend on it for Planet Legacy use cases. + token_file_opt: Optional[str] = None, # TODO: Remove? but we still depend on it for Planet Legacy use cases. # TODO?: initial_token_data: dict = None, save_token_file: bool = True, save_profile_config: bool = False, From a65dfc532872da212f269a237b0eabee5f5bc606 Mon Sep 17 00:00:00 2001 From: "Carl A. Adams" Date: Tue, 18 Mar 2025 20:30:26 -0700 Subject: [PATCH 6/9] linting --- src/planet_auth_utils/commands/cli/jwt_cmd.py | 6 +++--- src/planet_auth_utils/commands/cli/main.py | 1 - src/planet_auth_utils/commands/cli/oauth_cmd.py | 2 -- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/planet_auth_utils/commands/cli/jwt_cmd.py b/src/planet_auth_utils/commands/cli/jwt_cmd.py index 7c7e770..3a856bd 100644 --- a/src/planet_auth_utils/commands/cli/jwt_cmd.py +++ b/src/planet_auth_utils/commands/cli/jwt_cmd.py @@ -34,7 +34,7 @@ opt_token_file, opt_human_readable, ) -from .util import recast_exceptions_to_click, post_login_cmd_helper, print_obj +from .util import recast_exceptions_to_click class _jwt_human_dumps: @@ -193,7 +193,7 @@ def cmd_jwt_validate_oauth(ctx, token, token_file, audience, issuer, human_reada else: if not hazmat_body.get("iss"): raise click.BadParameter( - "The provided token does not contain an `iss` claim." " Is the provided JWT RFC 9068 compliant?" + "The provided token does not contain an `iss` claim. Is the provided JWT RFC 9068 compliant?" ) validation_iss = hazmat_body.get("iss") @@ -202,7 +202,7 @@ def cmd_jwt_validate_oauth(ctx, token, token_file, audience, issuer, human_reada else: if not hazmat_body.get("aud"): raise click.BadParameter( - "The provided token does not contain an `aud` claim." " Is the provided JWT RFC 9068 compliant?" + "The provided token does not contain an `aud` claim. Is the provided JWT RFC 9068 compliant?" ) hazmat_aud = hazmat_body.get("aud") if isinstance(hazmat_aud, list): diff --git a/src/planet_auth_utils/commands/cli/main.py b/src/planet_auth_utils/commands/cli/main.py index 6da6cb1..5111a6e 100644 --- a/src/planet_auth_utils/commands/cli/main.py +++ b/src/planet_auth_utils/commands/cli/main.py @@ -35,7 +35,6 @@ opt_show_qr_code, opt_sops, opt_audience, - opt_token_file, opt_scope, opt_yes_no, ) diff --git a/src/planet_auth_utils/commands/cli/oauth_cmd.py b/src/planet_auth_utils/commands/cli/oauth_cmd.py index 3b4f56e..90a57bc 100644 --- a/src/planet_auth_utils/commands/cli/oauth_cmd.py +++ b/src/planet_auth_utils/commands/cli/oauth_cmd.py @@ -21,9 +21,7 @@ OidcAuthClient, ExpiredTokenException, ClientCredentialsAuthClientBase, - TokenValidator, ) -from planet_auth.util import custom_json_class_dumper from .options import ( opt_audience, From bc03d40685bfa6ce8373a0f5db504b6bced2b782 Mon Sep 17 00:00:00 2001 From: "Carl A. Adams" Date: Tue, 18 Mar 2025 21:52:24 -0700 Subject: [PATCH 7/9] add discovery to the command line util --- src/planet_auth/auth_client.py | 20 +++++++++++++++++++ src/planet_auth/oidc/auth_client.py | 8 ++++++++ src/planet_auth_utils/__init__.py | 2 ++ .../commands/cli/oauth_cmd.py | 12 +++++++++++ 4 files changed, 42 insertions(+) diff --git a/src/planet_auth/auth_client.py b/src/planet_auth/auth_client.py index d2b4c79..dd6898f 100644 --- a/src/planet_auth/auth_client.py +++ b/src/planet_auth/auth_client.py @@ -450,6 +450,26 @@ def userinfo_from_access_token(self, access_token: str) -> dict: message="User information lookup is not implemented for the current authentication mechanism" ) + def oidc_discovery(self) -> dict: + """ + Query the authorization server's OIDC discovery endpoint for server information. + Returns: + Returns the OIDC discovery dictionary. + """ + raise AuthClientException( + message="OIDC discovery is not implemented for the current authentication mechanism." + ) + + # def oauth_discovery(self) -> dict: + # """ + # Query the authorization server's OAuth2 discovery endpoint for server information. + # Returns: + # Returns the OAuth2 discovery dictionary. + # """ + # raise AuthClientException( + # message="OAuth2 discovery is not implemented for the current authentication mechanism." + # ) + def get_scopes(self) -> List[str]: """ Query the authorization server for a list of scopes. diff --git a/src/planet_auth/oidc/auth_client.py b/src/planet_auth/oidc/auth_client.py index 69bc9f0..42314a2 100644 --- a/src/planet_auth/oidc/auth_client.py +++ b/src/planet_auth/oidc/auth_client.py @@ -588,6 +588,14 @@ def userinfo_from_access_token(self, access_token: str) -> dict: """ return self.userinfo_client().userinfo_from_access_token(access_token=access_token) + def oidc_discovery(self) -> dict: + """ + Query the authorization server's OIDC discovery endpoint for server information. + Returns: + Returns the OIDC discovery dictionary. + """ + return self._discovery() + def get_scopes(self) -> List[str]: """ Query the authorization server for a list of scopes. diff --git a/src/planet_auth_utils/__init__.py b/src/planet_auth_utils/__init__.py index 4957595..ea5f61e 100644 --- a/src/planet_auth_utils/__init__.py +++ b/src/planet_auth_utils/__init__.py @@ -39,6 +39,7 @@ cmd_oauth_revoke_access_token, cmd_oauth_revoke_refresh_token, cmd_oauth_userinfo, + cmd_oauth_discovery, cmd_oauth_list_scopes, cmd_oauth_print_access_token, ) @@ -103,6 +104,7 @@ "cmd_oauth_revoke_access_token", "cmd_oauth_revoke_refresh_token", "cmd_oauth_userinfo", + "cmd_oauth_discovery", "cmd_oauth_list_scopes", "cmd_oauth_print_access_token", "cmd_pllegacy", diff --git a/src/planet_auth_utils/commands/cli/oauth_cmd.py b/src/planet_auth_utils/commands/cli/oauth_cmd.py index 90a57bc..59a0fff 100644 --- a/src/planet_auth_utils/commands/cli/oauth_cmd.py +++ b/src/planet_auth_utils/commands/cli/oauth_cmd.py @@ -163,6 +163,18 @@ def cmd_oauth_list_scopes(ctx): print_obj([]) +@cmd_oauth.command("discovery") +@click.pass_context +@recast_exceptions_to_click(AuthException, FileNotFoundError) +def cmd_oauth_discovery(ctx): + """ + Look up OAuth server discovery information. + """ + auth_client = ctx.obj["AUTH"].auth_client() + discovery_json = auth_client.oidc_discovery() + print_obj(discovery_json) + + @cmd_oauth.command("validate-access-token") @click.pass_context @opt_human_readable From 62cbbd49d7c36790bf581346e625a7eee1661cc0 Mon Sep 17 00:00:00 2001 From: "Carl A. Adams" Date: Tue, 18 Mar 2025 22:36:50 -0700 Subject: [PATCH 8/9] hide unimplemented commands --- src/planet_auth_utils/commands/cli/jwt_cmd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/planet_auth_utils/commands/cli/jwt_cmd.py b/src/planet_auth_utils/commands/cli/jwt_cmd.py index 3a856bd..28141ec 100644 --- a/src/planet_auth_utils/commands/cli/jwt_cmd.py +++ b/src/planet_auth_utils/commands/cli/jwt_cmd.py @@ -225,7 +225,7 @@ def cmd_jwt_validate_oauth(ctx, token, token_file, audience, issuer, human_reada ) -@cmd_jwt.command("validate-rs256") +@cmd_jwt.command("validate-rs256", hidden=True) @click.pass_context @opt_human_readable @opt_token @@ -239,7 +239,7 @@ def cmd_jwt_validate_rs256(ctx, token, token_file, human_readable): raise NotImplementedError("Command not implemented") -@cmd_jwt.command("validate-hs512") +@cmd_jwt.command("validate-hs512", hidden=True) @click.pass_context @opt_human_readable @opt_token From c253c1a18440bb69f8b7efe87f4b35cf173b3b41 Mon Sep 17 00:00:00 2001 From: "Carl A. Adams" Date: Tue, 18 Mar 2025 22:43:38 -0700 Subject: [PATCH 9/9] WIP --- src/planet_auth_utils/__init__.py | 4 ++ src/planet_auth_utils/commands/cli/jwt_cmd.py | 44 +++++++++++++++++-- src/planet_auth_utils/commands/cli/options.py | 32 ++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/planet_auth_utils/__init__.py b/src/planet_auth_utils/__init__.py index ea5f61e..69a483d 100644 --- a/src/planet_auth_utils/__init__.py +++ b/src/planet_auth_utils/__init__.py @@ -64,6 +64,8 @@ opt_client_secret, opt_human_readable, opt_issuer, + opt_key, + opt_key_file, opt_loglevel, opt_long, opt_open_browser, @@ -124,6 +126,8 @@ "opt_client_secret", "opt_human_readable", "opt_issuer", + "opt_key", + "opt_key_file", "opt_loglevel", "opt_long", "opt_open_browser", diff --git a/src/planet_auth_utils/commands/cli/jwt_cmd.py b/src/planet_auth_utils/commands/cli/jwt_cmd.py index 28141ec..11f5939 100644 --- a/src/planet_auth_utils/commands/cli/jwt_cmd.py +++ b/src/planet_auth_utils/commands/cli/jwt_cmd.py @@ -14,6 +14,7 @@ import click import json +import jwt import pathlib import sys import textwrap @@ -30,6 +31,8 @@ from .options import ( opt_audience, opt_issuer, + opt_key, + opt_key_file, opt_token, opt_token_file, opt_human_readable, @@ -225,21 +228,54 @@ def cmd_jwt_validate_oauth(ctx, token, token_file, audience, issuer, human_reada ) -@cmd_jwt.command("validate-rs256", hidden=True) +@cmd_jwt.command("validate-rs256") @click.pass_context @opt_human_readable @opt_token @opt_token_file +@opt_key +@opt_key_file @recast_exceptions_to_click(AuthException, FileNotFoundError, NotImplementedError) -def cmd_jwt_validate_rs256(ctx, token, token_file, human_readable): +def cmd_jwt_validate_rs256(ctx, token, token_file, key, key_file, human_readable): """ Validate a JWT signed with a RS256 signature """ - # token_to_validate = _get_token_or_fail(token_opt=token, token_file_opt=token_file) + # The TokenValidator is geared for OAuth2 JWTs. + # This helper is lower level, and doesn't make the same assumptions. + # TODO: it might be nice to still have this more adjacent to the TokenValidator + # to keep practices aligned. + # validator = TokenValidator() + token_to_validate = _get_token_or_fail(token_opt=token, token_file_opt=token_file) + signing_key: jwt.PyJWK = None + # required_claims = [] + validated_complete = jwt.decode_complete( # Throws when invalid. + token_to_validate, + signing_key, + algorithms=["rs256", "RS256"], + # issuer=issuer, + # audience=audience, + options={ + # "require": required_claims, + # "verify_aud": True, + "verify_exp": True, + # "verify_iss": True, + "verify_signature": True, + }, + ) + # XXX check - validation throws on error + click.echo("TOKEN OK") + print_jwt_parts( + raw=token_to_validate, + header=validated_complete["header"], + body=validated_complete["payload"], + signature=validated_complete["signature"], + human_readable=human_readable, + ) + raise NotImplementedError("Command not implemented") -@cmd_jwt.command("validate-hs512", hidden=True) +@cmd_jwt.command("validate-hs512") @click.pass_context @opt_human_readable @opt_token diff --git a/src/planet_auth_utils/commands/cli/options.py b/src/planet_auth_utils/commands/cli/options.py index f90c28e..a430892 100644 --- a/src/planet_auth_utils/commands/cli/options.py +++ b/src/planet_auth_utils/commands/cli/options.py @@ -294,6 +294,38 @@ def opt_token_file(function): return function +def opt_key(function): + """ + Click option for specifying a key literal. + """ + function = click.option( + "--key", + help="Key string.", + type=str, + # envvar=EnvironmentVariables.AUTH_KEY, + show_envvar=False, + show_default=False, + )(function) + return function + + +def opt_key_file(function): + """ + Click option for specifying a key file location for the + planet_auth package's click commands. + """ + function = click.option( + "--key-file", + type=click.Path(exists=True, file_okay=True, readable=True, path_type=pathlib.Path), + # envvar=EnvironmentVariables.AUTH_KEY_FILE, + help="File containing a key.", + default=None, + show_envvar=False, + show_default=True, + )(function) + return function + + def opt_issuer(required=False): def decorator(function): """