diff --git a/docs/index.rst b/docs/index.rst index 703f80e..3c82750 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,90 +1,94 @@ .. _swh-web-client: .. include:: README.rst .. _swh-web-client-auth: Authentication -------------- If you have a user account registered on `Software Heritage Identity Provider`_, it is possible to authenticate requests made to the Web APIs through the use of -a OpenID Connect bearer token. Sending authenticated requests can notably +an OpenID Connect bearer token. Sending authenticated requests can notably allow to lift API rate limiting depending on your permissions. To get this token, a dedicated CLI tool is made available when installing ``swh-web-client``: .. code-block:: text $ swh auth Usage: swh auth [OPTIONS] COMMAND [ARGS]... Authenticate Software Heritage users with OpenID Connect. This CLI tool eases the retrieval of bearer tokens to authenticate a user querying the Software Heritage Web API. Options: --oidc-server-url TEXT URL of OpenID Connect server (default to "https://auth.softwareheritage.org/auth/") + --realm-name TEXT Name of the OpenID Connect authentication realm (default to "SoftwareHeritage") + --client-id TEXT OpenID Connect client identifier in the realm (default to "swh-web") + -h, --help Show this message and exit. Commands: - login Login and create new offline OpenID Connect session. - logout Logout from an offline OpenID Connect session. + generate-token Generate a new bearer token for Web API authentication. + revoke-token Revoke a bearer token used for Web API authentication. -In order to get your tokens, you need to use the ``login`` subcommand of -that CLI tool by passing your username as argument. You will be prompted +In order to get your tokens, you need to use the ``generate-token`` subcommand of +the CLI tool by passing your username as argument. You will be prompted for your password and if the authentication succeeds a new OpenID Connect -session will be created and tokens will be dumped to standard output. +offline session will be created and token will be dumped to standard output. .. code-block:: text - $ swh auth login + $ swh auth generate-token Password: eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJmNjMzMD... To authenticate yourself, you need to send that token value in request headers when querying the Web API. Considering you have stored that token value in a TOKEN environment variable, you can perform an authenticated call the following way using ``curl``: .. code-block:: text $ curl -H "Authorization: Bearer ${TOKEN}" https://archive.softwareheritage.org/api/1/ Note that if you intend to use the :class:`swh.web.client.client.WebAPIClient` class, you can activate authentication by using the following code snippet:: from swh.web.client.client import WebAPIClient TOKEN = '.......' # Use "swh auth login" command to get it client = WebAPIClient(bearer_token=TOKEN) # All requests to the Web API will be authenticated resp = client.get('swh:1:rev:aafb16d69fd30ff58afdd69036a26047f3aebdc6') -It is also possible to ``logout`` from the authenticated OpenID Connect session -which definitely revokes the token. +It is also possible to revoke a token, preventing future Web API authentication +when using it. The ``revoke-token`` subcommand of the CLI tool has to be used +to perform that task. .. code-block:: text - $ swh auth logout $REFRESH_TOKEN - Successfully logged out from OpenID Connect session + $ swh auth revoke-token $REFRESH_TOKEN + Token successfully revoked. API Reference ------------- .. toctree:: :maxdepth: 2 /apidoc/swh.web.client .. _Software Heritage Identity Provider: https://auth.softwareheritage.org/auth/realms/SoftwareHeritage/account/ diff --git a/swh/web/client/cli.py b/swh/web/client/cli.py index 4c53093..31f548a 100644 --- a/swh/web/client/cli.py +++ b/swh/web/client/cli.py @@ -1,97 +1,117 @@ # Copyright (C) 2020 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information # WARNING: do not import unnecessary things here to keep cli startup time under # control import click from click.core import Context from swh.core.cli import swh as swh_cli_group CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @swh_cli_group.group(name="auth", context_settings=CONTEXT_SETTINGS) @click.option( "--oidc-server-url", "oidc_server_url", default="https://auth.softwareheritage.org/auth/", help=( "URL of OpenID Connect server (default to " '"https://auth.softwareheritage.org/auth/")' ), ) @click.option( "--realm-name", "realm_name", default="SoftwareHeritage", help=( "Name of the OpenID Connect authentication realm " '(default to "SoftwareHeritage")' ), ) @click.option( "--client-id", "client_id", default="swh-web", help=("OpenID Connect client identifier in the realm " '(default to "swh-web")'), ) @click.pass_context def auth(ctx: Context, oidc_server_url: str, realm_name: str, client_id: str): """ Authenticate Software Heritage users with OpenID Connect. This CLI tool eases the retrieval of a bearer token to authenticate a user querying the Software Heritage Web API. """ from swh.web.client.auth import OpenIDConnectSession ctx.ensure_object(dict) ctx.obj["oidc_session"] = OpenIDConnectSession( oidc_server_url, realm_name, client_id ) -@auth.command("login") +@auth.command("generate-token") @click.argument("username") @click.pass_context -def login(ctx: Context, username: str): +def generate_token(ctx: Context, username: str): """ - Login and create new offline OpenID Connect session. + Generate a new bearer token for Web API authentication. Login with USERNAME, create a new OpenID Connect session and get bearer token. - User will be prompted for his password and tokens will be printed + User will be prompted for his password and token will be printed to standard output. The created OpenID Connect session is an offline one so the provided token has a much longer expiration time than classical OIDC sessions (usually several dozens of days). """ from getpass import getpass password = getpass() oidc_info = ctx.obj["oidc_session"].login(username, password) if "refresh_token" in oidc_info: print(oidc_info["refresh_token"]) else: print(oidc_info) -@auth.command("logout") +@auth.command("login", deprecated=True) +@click.argument("username") +@click.pass_context +def login(ctx: Context, username: str): + """ + Alias for 'generate-token' + """ + ctx.forward(generate_token) + + +@auth.command("revoke-token") @click.argument("token") @click.pass_context -def logout(ctx: Context, token: str): +def revoke_token(ctx: Context, token: str): """ - Logout from an offline OpenID Connect session. + Revoke a bearer token used for Web API authentication. Use TOKEN to logout from an offline OpenID Connect session. The token is definitely revoked after that operation. """ ctx.obj["oidc_session"].logout(token) - print("Successfully logged out from OpenID Connect session") + print("Token successfully revoked.") + + +@auth.command("logout", deprecated=True) +@click.argument("token") +@click.pass_context +def logout(ctx: Context, token: str): + """ + Alias for 'revoke-token' + """ + ctx.forward(revoke_token) diff --git a/swh/web/client/tests/test_cli.py b/swh/web/client/tests/test_cli.py index f76af36..9021cb0 100644 --- a/swh/web/client/tests/test_cli.py +++ b/swh/web/client/tests/test_cli.py @@ -1,50 +1,54 @@ # Copyright (C) 2020 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from click.testing import CliRunner from swh.web.client.cli import auth runner = CliRunner() oidc_profile = { "access_token": "some-access-token", "expires_in": 600, "refresh_expires_in": 0, "refresh_token": "some-refresh-token", "token_type": "bearer", "session_state": "some-state", "scope": "openid email profile offline_access", } -def test_auth_login(mocker): +def test_auth_generate_token(mocker): mock_getpass = mocker.patch("getpass.getpass") mock_getpass.return_value = "password" mock_oidc_session = mocker.patch("swh.web.client.auth.OpenIDConnectSession") mock_login = mock_oidc_session.return_value.login mock_login.return_value = oidc_profile - result = runner.invoke(auth, ["login", "username"], input="password\n") - assert result.exit_code == 0 - assert result.output[:-1] == oidc_profile["refresh_token"] + for command in ("generate-token", "login"): + mock_login.side_effect = None + result = runner.invoke(auth, [command, "username"], input="password\n") + assert result.exit_code == 0 + assert oidc_profile["refresh_token"] in result.output - mock_login.side_effect = Exception("Auth error") + mock_login.side_effect = Exception("Auth error") - result = runner.invoke(auth, ["login", "username"], input="password\n") - assert result.exit_code == 1 + result = runner.invoke(auth, [command, "username"], input="password\n") + assert result.exit_code == 1 -def test_auth_logout(mocker): +def test_auth_revoke_token(mocker): mock_oidc_session = mocker.patch("swh.web.client.auth.OpenIDConnectSession") mock_logout = mock_oidc_session.return_value.logout - result = runner.invoke(auth, ["logout", oidc_profile["refresh_token"]]) - assert result.exit_code == 0 + for command in ("revoke-token", "logout"): + mock_logout.side_effect = None + result = runner.invoke(auth, [command, oidc_profile["refresh_token"]]) + assert result.exit_code == 0 - mock_logout.side_effect = Exception("Auth error") - result = runner.invoke(auth, ["logout", oidc_profile["refresh_token"]]) - assert result.exit_code == 1 + mock_logout.side_effect = Exception("Auth error") + result = runner.invoke(auth, [command, oidc_profile["refresh_token"]]) + assert result.exit_code == 1