Skip to content

scitokens/scitokens-go

Repository files navigation

scitokens-go Go Reference

WORK IN PROGRESS library for handling SciTokens and WLCG tokens from Go, based on github.com/lestrrat-go/jwx libraries.

Included is a scitoken-validate command-line tool that uses the library to print and validate SciTokens with various criteria, see its README for installation and usage documentation.

The Enforcer API is believed to be stable, but breaking changes may still occur until version 1.0.0 is released.

Usage

To fetch and add the library to your Go project dependencies, run:

go get github.com/scitokens/scitokens-go

Then import it in your source:

import (
	scitokens "github.com/scitokens/scitokens-go"
)

Parsing SciTokens

Note: if you're only interested in validating tokens in a service, you can skip to the next section Validating Tokens, since the Enforcer abstracts away these details.

The SciToken interface is a light wrapper around the general Token interface from the github.com/lestrrat-go/jwx/jwt package, providing convenience methods for parsing and accessing SciToken-specific claims. After parsing the token into a jwt.Token you can convert it to an object implementing the SciToken interface with NewSciToken().

// PrintSciToken prints SciToken information to stdout, without doing any
// verification or validation of the token or its claims.
func PrintSciToken(tok []byte) error {
	jt, err := jwt.Parse(tok)
	if err != nil {
		return err
	}
	st, err := scitokens.NewSciToken(jt)
	if err != nil {
		return err
	}
	fmt.Println(st.Subject())
	fmt.Println(st.Issuer())
	fmt.Println(st.Scopes())
	fmt.Println(st.Groups())
	return nil
}

Validating Tokens

The Enforcer interface defines methods used to verify and validate tokens. Instantiate an enforcer with either NewEnforcer() for one-shot/throwaway use, or NewEnforcerDaemon() for long-lived processes. Both require one or more supported issuer URLs, and NewEnforcerDaemon additionally requires a context that defines the lifetime of the Enforcer and its background goroutines.

enf, err := scitokens.NewEnforcer("https://example.com")
if err != nil {
	log.Fatalf("failed to initialize enforcer: %s", err)
}

An enforcer instantiated with NewEnforcer() will fetch signing keys on-demand when a token is validated.

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
enf, err := scitokens.NewEnforcerDaemon(ctx, "https://example.com")
if err != nil {
	log.Fatalf("failed to initialize enforcer: %s", err)
}

An Enforcer instantiated with NewEnforcerDaemon() will fetch and cache the signing keys from the issuer, and start a goroutine that will routinely refresh the keys until the context is cancelled. Additional issuers can be added later with AddIssuer().

The enforcer provides a ValidateToken() method that verifies and validates a raw encoded token, and several convenience methods for validating tokens from a number of sources, including HTTP requests (ValidateTokenRequest()), and the execution environment (ValidateTokenEnvironment()).

By default the enforcer will verify that the token was signed by a trusted issuer, and that it passes basic validation criteria such as dates. It is not possible to directly parse a SciToken without performing these basic validation checks, this is by design, although it could change in the future if there is a good use case (the ValidateToken... function signatures and behavior won't change though).

Additional validation criteria can be attached to the enforcer with the following methods:

  • RequireAudience(), which takes a string representing the service's URL or some other identifier. It will check the tokens have this audience, or one of the standard wildcard audiences, currently "ANY" or "https://wlcg.cern.ch/jwt/v1/any". If not specified, the token audience will not be checked at all.

  • RequireScope(), which takes a Scope object that the token must have in the scope claim (pathed scopes will match exactly or for a hierarchical parent).

  • RequireGroup(), which takes a group name that must be in the wlcg.groups claim (group name must match exactly, but the leading slash is optional).

if err := enf.RequireAudience("https://example.com"); err != nil {
	log.Fatal(err)
}
if err := enf.RequireScope(scitokens.Scope{"compute.read", ""}); err != nil {
	log.Fatal(err)
}
if err := enf.RequireGroup("cms"); err != nil {
	log.Fatal(err)
}

Criteria set this way will apply to all future ValidateToken... calls. It's also possible to pass additional request-specific validation criteria to the ValidateToken... functions.

if _, err := enf.ValidateToken(tok, scitokens.WithGroup("cms/production")); err == nil {
	doRequest()
} else {
	e := &scitokens.TokenValidationError{}
	if !errors.As(err, &e) {
		// some internal error while parsing/validating the token
		log.Error(err)
	} else {
		// token is not valid, err (and e.Err) will say why.
		log.Debugf("access dened: %v", err)
	}
	denyRequest(err)
}

This example also demonstrates using errors.As() to check if the returned error is specifically a TokenValidationError due to the token not meeting some criteria, or some other internal error, which you may want to handle differently.

The ValidateToken... functions return a SciToken that can be inspected directly or passed to Validate() to test different criteria.

// request is valid if token has either /cms/production or /cms/operations group
if st, err := enf.ValidateToken(tok, scitokens.WithGroup("cms/production")); err == nil {
	doRequest()
} else if enf.Validate(st, scitokens.WithGroup("cms/operations")) == nil {
	doRequest()
} else {
	denyRequest(err)
}