luci: go.chromium.org/luci/server/tokens Index | Examples | Files

package tokens

import "go.chromium.org/luci/server/tokens"

Package tokens provides means to generate and validate base64 encoded tokens compatible with luci-py's components.auth implementation.

Code:

package main

import (
    "context"
    "fmt"
    "strings"
    "testing"
    "time"

    "go.chromium.org/luci/common/clock"
    "go.chromium.org/luci/common/clock/testclock"
    "go.chromium.org/luci/server/secrets"
    "go.chromium.org/luci/server/secrets/testsecrets"

    . "github.com/smartystreets/goconvey/convey"
    . "go.chromium.org/luci/common/testing/assertions"
)

func main() {
    kind := TokenKind{
        Algo:       TokenAlgoHmacSHA256,
        Expiration: 30 * time.Minute,
        SecretKey:  "secret_key_name",
        Version:    1,
    }

    ctx := context.Background()
    ctx, _ = testclock.UseTime(ctx, time.Unix(1444945245, 0))
    ctx = secrets.Set(ctx, &testsecrets.Store{})

    // Make a token.
    token, err := kind.Generate(ctx, []byte("state"), map[string]string{"k": "v"}, 0)
    if err != nil {
        fmt.Printf("error - %s\n", err)
        return
    }
    fmt.Printf("token - %s\n", token)

    // Validate it, extract embedded data.
    embedded, err := kind.Validate(ctx, token, []byte("state"))
    if err != nil {
        fmt.Printf("error - %s\n", err)
        return
    }
    fmt.Printf("embedded - %s\n", embedded)

}

func TestGenerate(t *testing.T) {
    kind := TokenKind{
        Algo:       TokenAlgoHmacSHA256,
        Expiration: 30 * time.Minute,
        SecretKey:  "secret_key_name",
        Version:    1,
    }

    Convey("Works", t, func() {
        ctx := testContext()
        token, err := kind.Generate(ctx, nil, nil, 0)
        So(token, ShouldNotEqual, "")
        So(err, ShouldBeNil)
    })

    Convey("Empty key", t, func() {
        ctx := testContext()
        token, err := kind.Generate(ctx, nil, map[string]string{"": "v"}, 0)
        So(token, ShouldEqual, "")
        So(err, ShouldErrLike, "empty key")
    })

    Convey("Forbidden key", t, func() {
        ctx := testContext()
        token, err := kind.Generate(ctx, nil, map[string]string{"_x": "v"}, 0)
        So(token, ShouldEqual, "")
        So(err, ShouldErrLike, "bad key")
    })

    Convey("Negative exp", t, func() {
        ctx := testContext()
        token, err := kind.Generate(ctx, nil, nil, -time.Minute)
        So(token, ShouldEqual, "")
        So(err, ShouldErrLike, "expiration can't be negative")
    })

    Convey("Unknown algo", t, func() {
        ctx := testContext()
        k2 := kind
        k2.Algo = "unknown"
        token, err := k2.Generate(ctx, nil, nil, 0)
        So(token, ShouldEqual, "")
        So(err, ShouldErrLike, "unknown algo")
    })
}

func TestValidate(t *testing.T) {
    kind := TokenKind{
        Algo:       TokenAlgoHmacSHA256,
        Expiration: 30 * time.Minute,
        SecretKey:  "secret_key_name",
        Version:    1,
    }

    Convey("Works", t, func() {
        ctx := testContext()
        token, err := kind.Generate(ctx, []byte("state"), map[string]string{
            "key1": "value1",
            "key2": "value2",
        }, 0)
        So(err, ShouldBeNil)

        // Good state.
        embedded, err := kind.Validate(ctx, token, []byte("state"))
        So(err, ShouldBeNil)
        So(embedded, ShouldResemble, map[string]string{
            "key1": "value1",
            "key2": "value2",
        })

        // Bad state.
        embedded, err = kind.Validate(ctx, token, []byte("???"))
        So(err, ShouldErrLike, "bad token MAC")

        // Not base64.
        embedded, err = kind.Validate(ctx, "?"+token[1:], []byte("state"))
        So(err, ShouldErrLike, "illegal base64 data")

        // Corrupted.
        embedded, err = kind.Validate(ctx, "X"+token[1:], []byte("state"))
        So(err, ShouldErrLike, "bad token MAC")

        // Too short.
        embedded, err = kind.Validate(ctx, token[:10], []byte("state"))
        So(err, ShouldErrLike, "too small")

        // Make it expired by rolling time forward.
        tc := clock.Get(ctx).(testclock.TestClock)
        tc.Add(31 * time.Minute)
        embedded, err = kind.Validate(ctx, token, []byte("state"))
        So(err, ShouldErrLike, "token expired")
    })

    Convey("Custom expiration time", t, func() {
        ctx := testContext()
        token, err := kind.Generate(ctx, nil, nil, time.Minute)
        So(err, ShouldBeNil)

        // Valid.
        _, err = kind.Validate(ctx, token, nil)
        So(err, ShouldBeNil)

        // No longer valid.
        tc := clock.Get(ctx).(testclock.TestClock)
        tc.Add(2 * time.Minute)
        _, err = kind.Validate(ctx, token, nil)
        So(err, ShouldErrLike, "token expired")
    })

    Convey("Unknown algo", t, func() {
        ctx := testContext()
        k2 := kind
        k2.Algo = "unknown"
        _, err := k2.Validate(ctx, "token", nil)
        So(err, ShouldErrLike, "unknown algo")
    })

    Convey("Padding", t, func() {
        // Produce tokens of various length to ensure base64 padding stripping
        // works.
        ctx := testContext()
        for i := 0; i < 10; i++ {
            data := map[string]string{
                "k": strings.Repeat("a", i),
            }
            token, err := kind.Generate(ctx, nil, data, 0)
            So(err, ShouldBeNil)
            extracted, err := kind.Validate(ctx, token, nil)
            So(err, ShouldBeNil)
            So(extracted, ShouldResemble, data)
        }
    })
}

func testContext() context.Context {
    ctx := context.Background()
    ctx, _ = testclock.UseTime(ctx, time.Unix(1444945245, 0))
    ctx = secrets.Set(ctx, &testsecrets.Store{})
    return ctx
}

Index

Examples

Package Files

tokens.go

Constants

const (
    // TokenAlgoHmacSHA256 algorithm stores public portion of the token as plain
    // text and uses HMAC SHA256 to authenticate its integrity.
    TokenAlgoHmacSHA256 = "HMAC-SHA256"
)

type TokenAlgo Uses

type TokenAlgo string

TokenAlgo identifies how token is authenticated.

type TokenKind Uses

type TokenKind struct {
    Algo       TokenAlgo
    Expiration time.Duration // how long generated token lives
    SecretKey  string        // name of the secret key in secrets.Store
    Version    byte          // tokens with another version will be rejected
}

TokenKind is a configuration of particular type of a token. It can be defined statically in a module and then its Generate() and Validate() methods can be used to produce and verify tokens.

func (*TokenKind) Generate Uses

func (k *TokenKind) Generate(c context.Context, state []byte, embedded map[string]string, exp time.Duration) (string, error)

Generate produces an urlsafe base64 encoded string that contains 'embedded' and MAC tag for 'state' + 'embedded' (but not the 'state' itself). The exact same 'state' then must be used in Validate to successfully verify the token.

'embedded' is an optional map with additional data to add to the token. It is embedded directly into the token and can be easily extracted from it by anyone who has the token. Should be used only for publicly visible data. It is tagged by token's MAC, so 'Validate' function can detect any modifications (and reject tokens tampered with).

The context is used to grab secrets.Store and the current time.

func (*TokenKind) Validate Uses

func (k *TokenKind) Validate(c context.Context, token string, state []byte) (map[string]string, error)

Validate checks token MAC and expiration, decodes data embedded into it.

'state' must be exactly the same as passed to Generate when creating a token. If it's different, the token is considered invalid. It usually contains some implicitly passed state that should be the same when token is generated and validated. For example, it may be an account ID of a current caller. Then if such token is used by another account, it is considered invalid.

The context is used to grab secrets.Store and the current time.

Package tokens imports 13 packages (graph) and is imported by 14 packages. Updated 2020-11-26. Refresh now. Tools for package owners.