scitokens / scitokens-go

Other
3 stars 1 forks source link

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:

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)
}