SciTokens Library

pypi Downloads per month License information

This library aims to be a reference implementation of the SciTokens’ JSON Web Token (JWT) token format.

SciTokens is built on top of the PyJWT and cryptography libraries. We aim to provide a safe, high-level interface for token manipulation, avoiding common pitfalls of using the underling libraries directly.

NOTE: SciTokens (the format and this library) is currently being designed; this README describes how we would like it to work, not necessarily current functionality. Particularly, we do not foresee the chained tokens described here as part of the first release’s functionality. The ideas behind the separate Validator in this library is taken from libmacaroons.

Generating Tokens

Usage revolves around the SciToken object. This can be generated directly:

>>> import scitokens
>>> token = scitokens.SciToken() # Create token and generate a new private key
>>> token2 = scitokens.SciToken(key=private_key) # Create token using existing key

where key is a private key object (more later on generating private keys). Direct generation using a private key will most often be done to do a base token. SciTokens can be chained, meaning one token can be appended to another:

>>> token = scitokens.SciToken(parent=parent_token)

The generated object, token, will default to having all the authoriations of the parent token - but is mutable and can add further restrictions.

Tokens contain zero or more claims, which are facts about the token that typically indicate some sort of authorization the bearer of the token has. A token has a list of key-value pairs; each token can only have a single value per key, but multiple values per key can occur in a token chain.

To set a claim, one can use dictionary-like setter:

>>> token['claim1'] = 'value2'

The value of each claim should be a Python object that can be serialized to JSON.

Token Serialization

Parent tokens are typically generated by a separate server and sent as a response to a successful authentication or authorization request. SciTokens are built on top of JSON Web Tokens (JWT), which define a useful base64-encoded serialization format. A serialized token may look something like this:

eyJhbGciOiJFUzI1NiIsImN3ayI6eyJ5IjoiazRlM1FFeDVjdGJsWmNrVkhINlkzSFZoTzFadUxVVWNZQW5ON0xkREV3YyIsIngiOiI4TkU2ZEE2T1g4NHBybHZEaDZUX3kwcWJOYmc5a2xWc2pYQnJnSkw5aElBIiwiY3J2IjoiUC0yNTYiLCJrdHkiOiJFQyJ9LCJ0eXAiOiJKV1QiLCJ4NXUiOiJodHRwczovL3ZvLmV4YW1wbGUuY29tL0pXUyJ9.eyJyZWFkIjoiL2xpZ28ifQ.uXVzbcOBCK4S4W89HzlWNmnE9ZcpuRHKTrTXYv8LZL9cDy3Injf97xNPm756fKcYwBO5KykYngFrUSGa4owglA.eyJjcnYiOiAiUC0yNTYiLCAia3R5IjogIkVDIiwgImQiOiAieWVUTTdsVXk5bGJEX2hnLVVjaGp0aXZFWHZxSWxoelJQVEVaZDBaNFBpOCJ9

This is actually 4 separate base64-encoded strings, separated by the . character. The four pieces are:

  • A header, implementing the JSON Web Key standard, specifying the cryptographic properties of the token.

  • A payload, specifying the claims (key-value pairs) encoded by the token and asserted by the VO.

  • A signature of the header and payload, ensuring authenticity of the payload.

  • A key, utilized to sign any derived tokens. The key is an optional part of the token format, but may be required by some remote services.

Given a serialized token, the scitokens library can deserialize it:

>>> token = scitokens.SciToken.deserialize(token_serialized_bytes)

As part of the deserialization, the scitokens library will throw an exception if token verification failed.

The existing token can be serialized with the serialize method:

>>> token_serialized_bytes = token.serialize()

Validating Tokens

In SciTokens, we try to distinguish between validating and verifying tokens. Here, verification refers to determining the integrity and authenticity of the token: can we validate the token came from a known source without tampering? Can we validate the chain of trust? Validation is determining whether the claims of the token are satisfied in a given context.

For example, if a token contains the claims {vo: ligo, op: read, path: /ligo}, we would first verify that the token is correctly signed by a known public key associated with LIGO. When presented to a storage system along with an HTTP request, the storage system would validate the token authorizes the corresponding request (is it a GET request? Is it for a sub-path of /ligo?).

Within the scitokens module, validation is done by the Validator object:

>>> val = scitokens.Validator()

This object can be reused for multiple validations. All SciToken claims must be validated. There are no “optional” claim attributes or values.

To validate a specific claim, provide a callback function to the Validator object:

>>> def validate_op(value):
...     return value == True
>>> val.add_validator("op", validate_op)

Once all the known validator callbacks have been registered, use the validate method with a token:

>>> val.validate(token)

This will throw a ValidationException if the token could not be validated.

Enforcing SciTokens Logic

For most users of SciTokens, determining that a token is valid is insufficient. Rather, most will be asking “does this token allow the current resource request?” The valid token must be compared to some action the user is attempting to take.

To assist in the authorization enforcement, the SciTokens library provides the Enforcer class.

An unique Enforcer object is needed for each thread and issuer:

>>> enf = scitokens.Enforcer("https://scitokens.org/dteam")

This object will accept tokens targetted to any audience; a more typical use case will look like the following:

>>> enf = scitokens.Enforcer("https://scitokens.org/dteam",
                             audience="https://example.com")

This second enforcer would not accept tokens that are intended for https://google.com.

The enforcer can then test authorization logic against a valid token:

>>> token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtp..."
>>> stoken = scitokens.SciToken.deserialize(token)
>>> enf.generate_acls(stoken)
[(u'write', u'/store/user/bbockelm'), (u'read', u'/store')]
>>> enf.test(stoken, "read", "/store/foo")
True
>>> enf.test(stoken, "write", "/store/foo")
False
>>> enf.test(stoken, "write", "/store/user/foo")
False
>>> enf.test(stoken, "write", "/store/user/bbockelm/foo")
True

The test method uses the SciTokens built-in path parsing to validate the authorization. The generate_acls method allows the caller to cache the ACL information from the token.

Creating Sample Tokens

Typically, an access token is generated during an OAuth2 workflow to facilitate authentication and authorization. However, for testing and experimentation purposes, the demo token generator provides users with the ability to create sample tokens with customized payload:

>>> payload = {"sub": "<email adress>", "scope": "read:/protected"}
>>> token = scitokens.utils.demo.token(payload)

The token method makes a request to the generator to create a serialized token for the specified payload. Users can also retrieve a parsed token by calling the parsed_token method, which returns a SciToken object corresponding to the token. The object contains the decoded token data, including the claims and signature.

Decorator

This protect decorator is designed to be used with a flask application. It can be used like:

@scitokens_protect.protect(audience="https://demo.scitokens.org", scope="read:/secret", issuer="https://demo.scitokens.org")
def Secret(token: SciToken):
    # ... token is now available.

The possible arguments are:

  • audience (str or list): Audience expected in the client token

  • scope (str): Scope required to access the function

  • issuer (str): The issuer to require of the client token

The protected function can optionally take an argument token, which is the parsed SciToken object.

Configuration

An optional configuration file can be provided that will alter the behavior of the SciTokens library. Configuration options include:

Key

Description

log_level

The log level for which to use. Options include: CRITICAL, ERROR, WARNING, INFO, DEBUG. Default: WARNING

log_file

The full path to the file to log. Default: None

cache_lifetime

The minimum lifetime (in seconds) of keys in the keycache. Default: 3600 seconds

cache_location

The directory to store the KeyCache, used to store public keys across executions. Default: $HOME/.cache/scitokens

The configuration file is in the ini format, and will look similar to:

[scitokens]
log_level = DEBUG
cache_lifetime = 60

You may set the configuration by passing a file name to scitokens.set_config function:

>> import scitokens
>> scitokens.set_config("/etc/scitokens/scitokens.ini")

Project Status

pypi Build pipeline status Code coverage Code Quality Documentation Status

API Reference

Indices and tables