How to Create A Minimal Script without GlobusApp

GlobusApp provides a number of useful abstractions in the SDK. It handles login flows and storage of tokens, coupled with later retrieval of those tokens for use. It can keep track of which clients have been created and registered with an app, and therefore make intelligent decisions about how and when to prompt users to login.

New users should read the guide for writing a minimal script before reading this doc.

GlobusApp is built from several simpler components which can be used to implement similar behaviors. This doc covers how to write a simple script, but without using GlobusApp.

For readers who prefer to start with complete examples, jump ahead to these sections before reviewing the doc:

Cases for not Using GlobusApp

There are at least three main reasons to be interested in defining applications without GlobusApp:

  1. You have a use case which doesn’t fit the behaviors of an app. e.g., Implementations of APIs or services.

  2. Any legacy codebase, predating GlobusApp, will use the underlying constructs to implement login behaviors.

  3. Customizing and extending GlobusApp to suit your use case may require understanding the underlying components.

Note

Prior to the introduction of GlobusApp, these tools were the only way that an application could be written with the globus_sdk. If you are maintaining an existing application, you may need to be strategic about when and how to upgrade such usages.

Define an Auth Client Instance

In order to interact with Globus Auth, you will need a client object. This will be the driver of the login flow.

import globus_sdk

# this is the tutorial client ID
# replace this string with your ID for production use
CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"

# create a client for interactions with Globus Auth
auth_client = globus_sdk.NativeAppAuthClient(CLIENT_ID)

This uses a NativeAppAuthClient, which is one of the two types of client object supporting logins. The other is ConfidentialAppAuthClient, which is for clients which authenticate themselves using a stored secret.

There are differences in behavior between the two types of Auth client, but NativeAppAuthClient is more appropriate for scripts which you may later redistribute.

Create and Use a GroupsClient

To make use of the tokens procured in the previous step, you’ll need to create a client object, pass it the appropriate token data, and use it to call out to the Globus Groups API.

Credentials are passed through a generic “authorizer” interface which allows the tokens to be passed statically or as a reference to some dynamic data source.

# extract tokens from the response which match Globus Groups
groups_tokens = tokens.by_resource_server[globus_sdk.GroupsClient.resource_server]

# construct an AccessTokenAuthorizer and use it to construct the GroupsClient
groups_client = globus_sdk.GroupsClient(
    authorizer=globus_sdk.AccessTokenAuthorizer(groups_tokens["access_token"])
)

# call out to the Groups service to get a listing
my_groups = groups_client.get_my_groups()

# print in CSV format
print("ID,Name,Roles")
for group in my_groups:
    roles = "|".join({m["role"] for m in group["my_memberships"]})
    print(",".join([group["id"], f'"{group["name"]}"', roles]))

Recap: List Groups Script

The previous sections can be combined into a working script.

The following example is complete. It should run without modification “as-is”.

list_groups_noapp.py [download]
import globus_sdk

# this is the tutorial client ID
# replace this string with your ID for production use
CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"

# create a client for interactions with Globus Auth
auth_client = globus_sdk.NativeAppAuthClient(CLIENT_ID)

# using that client, do a login flow for Globus Groups credentials
auth_client.oauth2_start_flow(
    requested_scopes=globus_sdk.GroupsClient.scopes.view_my_groups_and_memberships
)
authorize_url = auth_client.oauth2_get_authorize_url()
print(f"Please go to this URL and login:\n\n{authorize_url}\n")

auth_code = input("Please enter the code here: ").strip()
tokens = auth_client.oauth2_exchange_code_for_tokens(auth_code)

# extract tokens from the response which match Globus Groups
groups_tokens = tokens.by_resource_server[globus_sdk.GroupsClient.resource_server]

# construct an AccessTokenAuthorizer and use it to construct the GroupsClient
groups_client = globus_sdk.GroupsClient(
    authorizer=globus_sdk.AccessTokenAuthorizer(groups_tokens["access_token"])
)

# call out to the Groups service to get a listing
my_groups = groups_client.get_my_groups()

# print in CSV format
print("ID,Name,Roles")
for group in my_groups:
    roles = "|".join({m["role"] for m in group["my_memberships"]})
    print(",".join([group["id"], f'"{group["name"]}"', roles]))

Adding in Refresh Tokens & Token Storage

To expand upon this example, it is possible to request long-lived tokens called “refresh tokens”, which are valid until they are revoked or go unused for a long period. Making use of refresh tokens is most appropriate if we also store the tokens between runs of the script, so that we can reuse the tokens.

Refresh tokens operate by getting an access token, like the example above, but allowing you to automatically replace or “refresh” that token any time it expires. We will therefore also need to elaborate our usage to handle these automatic refreshes.

Requesting Refresh Tokens

To request refresh tokens, simply pass refresh_tokens=True to the oauth2_start_flow call:

auth_client.oauth2_start_flow(
    requested_scopes=globus_sdk.GroupsClient.scopes.view_my_groups_and_memberships,
    refresh_tokens=True,
)

Defining Token Storage

Token storage abstractions are defined in the SDK which provide the ability to read or write token data in a structured way. Defining a token storage object is simple:

import os
from globus_sdk.tokenstorage import JSONTokenStorage

token_storage = JSONTokenStorage(
    os.path.expanduser("~/.list-my-globus-groups-tokens.json")
)

Linking a Login Flow to Token Storage

To connect the tokens from login to a token storage, use the token storage method store_token_response() at the end of the login flow.

And in order to make the script only prompt for login if there are no tokens, we can ask the JSONTokenStorage.file_exists() method whether or not there is a file. This rewrites our login block to be nested under a file_exists() check:

# if there is no stored token file, we have not yet logged in
if not token_storage.file_exists():
    # do a login flow, getting back a token response
    auth_client.oauth2_start_flow(
        requested_scopes=globus_sdk.GroupsClient.scopes.view_my_groups_and_memberships,
        refresh_tokens=True,
    )
    authorize_url = auth_client.oauth2_get_authorize_url()
    print(f"Please go to this URL and login:\n\n{authorize_url}\n")
    auth_code = input("Please enter the code here: ").strip()
    token_response = auth_client.oauth2_exchange_code_for_tokens(auth_code)
    # now store the tokens
    token_storage.store_token_response(token_response)

Building a RefreshTokenAuthorizer

Token storage defines how the data gets stored, and how it is retrieved. The storage is also integral to how refresh tokens are used – we need a place to store updated tokens whenever we have a refresh.

We will load the groups token out of the token storage and use it to construct a RefreshTokenAuthorizer, which handles automatic refreshes. To write updated tokens back into the storage, we pass it back into the authorizer, like so:

# load the tokens from the storage -- either freshly stored or loaded from disk
token_data = token_storage.get_token_data(globus_sdk.GroupsClient.resource_server)

# construct the RefreshTokenAuthorizer which writes back to storage on refresh
authorizer = globus_sdk.RefreshTokenAuthorizer(
    token_data.refresh_token,
    auth_client,
    access_token=token_data.access_token,
    expires_at=token_data.expires_at_seconds,
    on_refresh=token_storage.store_token_response,
)

Construct and Use the GroupsClient

Now that we have a new authorizer, it is simple to construct and use a new client, just as before:

# use that authorizer to authorize the activity of the groups client
groups_client = globus_sdk.GroupsClient(authorizer=authorizer)

# call out to the Groups service to get a listing
my_groups = groups_client.get_my_groups()

# print in CSV format
print("ID,Name,Roles")
for group in my_groups:
    roles = "|".join({m["role"] for m in group["my_memberships"]})
    print(",".join([group["id"], f'"{group["name"]}"', roles]))

Recap: List Groups with RefreshTokens

As a complete example of the List Groups script with token storage and a refresh token authorizer, the above sections can be combined into the following script:

The following example is complete. It should run without modification “as-is”.

list_groups_noapp_with_storage.py [download]
import os

import globus_sdk
from globus_sdk.tokenstorage import JSONTokenStorage

# this is the tutorial client ID
# replace this string with your ID for production use
CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"

# define a client for interactions with Globus Auth
auth_client = globus_sdk.NativeAppAuthClient(CLIENT_ID)

# define token storage where tokens will be stored between runs
token_storage = JSONTokenStorage(
    os.path.expanduser("~/.list-my-globus-groups-tokens.json")
)


# if there is no stored token file, we have not yet logged in
if not token_storage.file_exists():
    # do a login flow, getting back a token response
    auth_client.oauth2_start_flow(
        requested_scopes=globus_sdk.GroupsClient.scopes.view_my_groups_and_memberships,
        refresh_tokens=True,
    )
    authorize_url = auth_client.oauth2_get_authorize_url()
    print(f"Please go to this URL and login:\n\n{authorize_url}\n")
    auth_code = input("Please enter the code here: ").strip()
    token_response = auth_client.oauth2_exchange_code_for_tokens(auth_code)
    # now store the tokens
    token_storage.store_token_response(token_response)

# load the tokens from the storage -- either freshly stored or loaded from disk
token_data = token_storage.get_token_data(globus_sdk.GroupsClient.resource_server)

# construct the RefreshTokenAuthorizer which writes back to storage on refresh
authorizer = globus_sdk.RefreshTokenAuthorizer(
    token_data.refresh_token,
    auth_client,
    access_token=token_data.access_token,
    expires_at=token_data.expires_at_seconds,
    on_refresh=token_storage.store_token_response,
)

# use that authorizer to authorize the activity of the groups client
groups_client = globus_sdk.GroupsClient(authorizer=authorizer)

# call out to the Groups service to get a listing
my_groups = groups_client.get_my_groups()

# print in CSV format
print("ID,Name,Roles")
for group in my_groups:
    roles = "|".join({m["role"] for m in group["my_memberships"]})
    print(",".join([group["id"], f'"{group["name"]}"', roles]))