dynoid

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 24, 2024 License: BSD-3-Clause Imports: 12 Imported by: 0

README

[!IMPORTANT] DynoID is currently a Heroku Labs feature and is not enabled by default for all spaces.

DynoID

DynoID is a feature of Heroku Private Spaces that leverages OpenID Connect to mint tokens for each dyno that it can use to authenticate and verify their identity to other services.

Audiences

All dynos get a heroku audience token by default. Additional audiences can be minted by setting the HEROKU_DYNO_ID_AUDIENCES config var to a comma separated list of audiences.

To Authenticate Calls to Your Service

The dynoid package provides all of the functions needed to verify a token issued for an app in a Heroku Private Space. Additionally there is a middleware package that provides a set of http handlers and middleware suitable for use in a web application.

Direct Verification

In the case that you want to verify a token outside of an http.Handler you can leverage the Verifier directly.

HTTP Middleware

The dynoid/middleware package provides several net/http middleware that validate incoming requests are authenticated and adds the parsed token to the request context to be used further down the stack.

Testing and Local Development

The dynoidtest package provides a number of functions useful for testing and local development.

dynoid

import "github.com/heroku/x/dynoid"

Index

Variables

DefaultFS is used by ReadLocal and ReadLocalToken to retrieve tokens.

By default they are retrieved via os.Open and os.ReadFile.

This is useful when testing code that uses DynoID.

var DefaultFS fs.ReadFileFS = &osReader{}

var (
    ErrTokenNotSet = errors.New("token not set") // returned when neither a token nor an error is set
)

func ContextWithError

func ContextWithError(ctx context.Context, err error) context.Context

ContextWithError adds the given error to the Context to be retrieved later by calling FromContext

func ContextWithToken

func ContextWithToken(ctx context.Context, t *Token) context.Context

ContextWithToken adds the given Token to the Context to be retrieved later by calling FromContext

func LocalTokenPath

func LocalTokenPath(audience string) string

LocalTokenPath returns the path on disk to the token for the given audience

func ReadLocal

func ReadLocal(audience string) (string, error)

ReadLocal reads the local machines token for the given audience

Suitable for passing as a bearer token

type IssuerCallback

An IssuerCallback is called whenever a token is verified to ensure it matches some expected criteria.

type IssuerCallback func(issuer string) error

func AllowHerokuHost
func AllowHerokuHost(herokuHost string) IssuerCallback

AllowHerokuHost verifies that the issuer is from Heroku for the given host domain

func AllowHerokuSpace
func AllowHerokuSpace(herokuHost string, spaceIDs ...string) IssuerCallback

AllowHerokuSpace verifies that the issuer is from Heroku for the given host and space id.

type MalformedTokenError

Returned when the token doesn't match the expected format

type MalformedTokenError struct {
    // contains filtered or unexported fields
}

func (*MalformedTokenError) Error
func (e *MalformedTokenError) Error() string

func (*MalformedTokenError) Unwrap
func (e *MalformedTokenError) Unwrap() error

type Subject

Subject contains information about the app and dyno the token was issued for

type Subject struct {
    AppID   string `json:"app_id"`
    AppName string `json:"app_name"`
    Dyno    string `json:"dyno"`
}

func (*Subject) LogValue
func (s *Subject) LogValue() slog.Value

func (*Subject) MarshalText
func (s *Subject) MarshalText() ([]byte, error)

func (*Subject) String
func (s *Subject) String() string

func (*Subject) UnmarshalText
func (s *Subject) UnmarshalText(text []byte) error

type Token

Token contains all of the token information stored by Heroku when it's issued

type Token struct {
    IDToken *oidc.IDToken `json:"-"`
    SpaceID string        `json:"space_id"`
    Subject *Subject      `json:"subject"`
}

func FromContext
func FromContext(ctx context.Context) (*Token, error)

FromContext returns the Token or error associated with the given Context

func ReadLocalToken
func ReadLocalToken(ctx context.Context, audience string) (*Token, error)

ReadLocalToken reads the local machines token for the given audience and parses it

func (*Token) LogValue
func (t *Token) LogValue() slog.Value

type UntrustedIssuerError

Returned by an IssuerCallback getting an issuer it doesn't trust

type UntrustedIssuerError struct {
    Issuer string
}

func (*UntrustedIssuerError) Error
func (e *UntrustedIssuerError) Error() string

type Verifier

A Verifier verifies a raw token with it's oids issuer and uses the IssuerCallback to ensure it's from a trusted source.

type Verifier struct {
    IssuerCallback IssuerCallback
    // contains filtered or unexported fields
}
Example

package main

import (
	"fmt"

	"github.com/heroku/x/dynoid"
	"github.com/heroku/x/dynoid/internal"
)

const Audience = "testing"

func main() {
	// Normally a token would be passed in, but for testing we'll generate one
	ctx, token := internal.GenerateToken(Audience)

	verifier := dynoid.New(Audience)
	verifier.IssuerCallback = dynoid.AllowHerokuHost("heroku.local") // heroku.com for production

	t, err := verifier.Verify(ctx, token)
	if err != nil {
		fmt.Printf("failed to verify token (%v)", err)
		return
	}

	fmt.Println(t.Subject.AppID)
	fmt.Println(t.Subject.AppName)
	fmt.Println(t.Subject.Dyno)
}
Output
00000000-0000-0000-0000-000000000001
sushi
web.1

func New
func New(audience string) *Verifier

Instantiate a new Verifier without an IssuerCallback set.

The IssuerCallback must be set before calling Verify or an error will be returned.

func NewWithCallback
func NewWithCallback(audience string, callback IssuerCallback) *Verifier

Instantiate a new Verifier with the IssuerCallback set.

func (*Verifier) Verify
func (v *Verifier) Verify(ctx context.Context, rawIDToken string) (*Token, error)

Verify validates the given token with the OIDC provider and validates it against the IssuerCallback

dynoidtest

import "github.com/heroku/x/dynoid/dynoidtest"

dynoidtest provides helper functions for testing code that uses DynoID

Index

Constants

const (
    DefaultHerokuHost = "heroku.local"                         // issuer host used when one is not provided
    DefaultSpaceID    = "test"                                 // space id used when one is not provided
    DefaultAppID      = "00000000-0000-0000-0000-000000000001" // app id used when one is not provided
    DefaultAppName    = "sushi"                                // app name used when one is not provided
    DefaultDyno       = "web.1"                                // dyno used when one is not provided
)

func GenerateDefaultFS

func GenerateDefaultFS(iss *Issuer, audiences ...string) error

Configure dynoid.DefaultFS to use tokens generated by the provided issuer for the audiences listed.

func Issue

func Issue(iss *Issuer) http.Handler

The Issue http.Handler generates test tokens via a local Issuer using the provided opts.

A query param 'audience' is expected.

func LocalIssuer

func LocalIssuer(iss *Issuer) func(http.Handler) http.Handler

Configures the oidc client to use the issuer provided.

type FS

FS implements fs.ReadFileFS and is suitable for testing reading tokens.

type FS struct {
    // contains filtered or unexported fields
}

func NewFS
func NewFS(tokens map[string]string) *FS

Create a new FS where the DynoID tokens have been populated.

The tokens map keys are the expected audience and the values are the token contents.

func (*FS) Open
func (f *FS) Open(name string) (fs.File, error)

func (*FS) ReadFile
func (f *FS) ReadFile(name string) ([]byte, error)

type Issuer

Issuer generates test tokens and provides a client for verifying them.

type Issuer struct {
    // contains filtered or unexported fields
}
Example

package main

import (
	"context"
	"fmt"

	"github.com/heroku/x/dynoid"
	"github.com/heroku/x/dynoid/dynoidtest"
)

const Audience = "testing"

func main() {
	ctx, iss, err := dynoidtest.NewWithContext(context.Background())
	if err != nil {
		panic(err)
	}

	if err := dynoidtest.GenerateDefaultFS(iss, Audience); err != nil {
		panic(err)
	}

	token, err := dynoid.ReadLocalToken(ctx, Audience)
	if err != nil {
		panic(err)
	}

	fmt.Println(token.Subject.AppID)
	fmt.Println(token.Subject.AppName)
	fmt.Println(token.Subject.Dyno)
}
Output
00000000-0000-0000-0000-000000000001
sushi
web.1

func New
func New(opts ...IssuerOpt) (*Issuer, error)

Create a new Issuer with the supplied opts applied

func NewWithContext
func NewWithContext(ctx context.Context, opts ...IssuerOpt) (context.Context, *Issuer, error)

Create a new Issuer with the supplied opts applied inheriting from the provided context

func (*Issuer) GenerateIDToken
func (iss *Issuer) GenerateIDToken(audience string, opts ...TokenOpt) (string, error)

GenerateIDToken returns a new signed token as a string

func (*Issuer) HTTPClient
func (iss *Issuer) HTTPClient() *http.Client

HTTPClient returns a client that leverages the Issuer to validate tokens.

type IssuerOpt

IssuerOpt allows the behavior of the issuer to be modified.

type IssuerOpt interface {
    // contains filtered or unexported methods
}

func WithHerokuHost
func WithHerokuHost(herokuHost string) IssuerOpt

WithHerokuHost allows an issuer host to be supplied instead of using the default

func WithKey
func WithKey(key *rsa.PrivateKey) IssuerOpt

WithKey allows you to set the issuer's private key. Useful for leveraging test middleware.

func WithSpaceID
func WithSpaceID(spaceID string) IssuerOpt

WithSpaceID allows a spaceID to be supplied instead of using the default

func WithTokenOpts
func WithTokenOpts(opts ...TokenOpt) IssuerOpt

WithTokenOpts allows a default set of TokenOpt to be applied to every token generated by the issuer

type LocalConfiguration

LocalConfiguration provides methods for working with a local issuer configured with ConfigureLocal

type LocalConfiguration struct {
    // contains filtered or unexported fields
}

func ConfigureLocal
func ConfigureLocal(audiences []string, opts ...IssuerOpt) (*LocalConfiguration, error)

ConfigureLocal sets up the environment with a local DynoID issuer and generates tokens for the audiences provided.

The returned LocalConfiguration provides methods for working with the issuer.

func ConfigureLocalWithContext
func ConfigureLocalWithContext(ctx context.Context, audiences []string, opts ...IssuerOpt) (*LocalConfiguration, error)

func (*LocalConfiguration) Context
func (c *LocalConfiguration) Context() context.Context

func (*LocalConfiguration) GenerateToken
func (c *LocalConfiguration) GenerateToken(audience string) (string, error)

GenerateToken mints a token for the given audience using the configured issuer.

func (*LocalConfiguration) Handler
func (c *LocalConfiguration) Handler() http.Handler

Handler mints tokens for the configured issuer using for the audience specified by the audience query param.

func (*LocalConfiguration) Middleware
func (c *LocalConfiguration) Middleware() func(http.Handler) http.Handler

The Middleware should be inserted in the middleware stack before any functions that use dynoid are called.

type TokenOpt

A TokenOpt modifies the way a token is minted

type TokenOpt interface {
    // contains filtered or unexported methods
}

func WithSubject
func WithSubject(s *dynoid.Subject) TokenOpt

WithSubject allows the Subject to be different than the default

func WithSubjectFunc
func WithSubjectFunc(fn func(audience string, subject *dynoid.Subject) *dynoid.Subject) TokenOpt

WithSubjectFunc allows the Subject to be different than the default based on the audience being generated.

internal

import "github.com/heroku/x/dynoid/internal"

Index

func GenerateToken

func GenerateToken(audience string) (context.Context, string)

middleware

import "github.com/heroku/x/dynoid/middleware"
Example

package main

import (
	"io"
	"log"
	"net/http"

	"github.com/heroku/x/dynoid/middleware"
)

const Audience = "testing"

func main() {
	authorized := middleware.AuthorizeSameSpace(Audience)
	secureHandler := authorized(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if _, err := io.WriteString(w, "Hello from a secure endpoint!\n"); err != nil {
			log.Printf("error writing response (%v)", err)
		}
	}))

	http.Handle("/secure", secureHandler)

	log.Fatal(http.ListenAndServe(":8080", nil))
}

Index

Variables

var (
    // returned when the `Authorization` header does not contain a Bearer token
    ErrTokenMissing = errors.New("token not found")
)

func Authorize

func Authorize(audience string, callback dynoid.IssuerCallback) func(http.Handler) http.Handler

Authorize populates the dyno identity blocks requests where the callback fails.

func AuthorizeSameSpace

func AuthorizeSameSpace(audience string) func(http.Handler) http.Handler

AuthorizeSameSpace restricts access to tokens from the same space/issuer for the given audience.

func AuthorizeSpaces

func AuthorizeSpaces(audience string, spaces ...string) func(http.Handler) http.Handler

AuthorizeSpaces populates the dyno identity and blocks any requests that aren't from one of the given spaces.

func AuthorizeSpacesWithIssuer

func AuthorizeSpacesWithIssuer(audience, issuer string, spaces ...string) func(http.Handler) http.Handler

AuthorizeSpacesWithIssuer populates the dyno identity and blocks any requests that aren't from one of the given spaces and issuer.

func Populate

func Populate(audience string, callback dynoid.IssuerCallback) func(http.Handler) http.Handler

Populate attempts to validate and parse a Token from the request for the given audience but doesn't enforce any restrictions.

Generated by gomarkdoc

Documentation

Index

Examples

Constants

View Source
const (
	ErrMustCheckIssuer staticError = "must check issuer"
)

Variables

View Source
var DefaultFS fs.ReadFileFS = &osReader{}

DefaultFS is used by ReadLocal and ReadLocalToken to retrieve tokens.

By default they are retrieved via os.Open and os.ReadFile.

This is useful when testing code that uses DynoID.

View Source
var (
	ErrTokenNotSet = errors.New("token not set") // returned when neither a token nor an error is set
)

Functions

func ContextWithError added in v0.4.0

func ContextWithError(ctx context.Context, err error) context.Context

ContextWithError adds the given error to the Context to be retrieved later by calling FromContext

func ContextWithToken added in v0.4.0

func ContextWithToken(ctx context.Context, t *Token) context.Context

ContextWithToken adds the given Token to the Context to be retrieved later by calling FromContext

func LocalTokenPath added in v0.2.0

func LocalTokenPath(audience string) string

LocalTokenPath returns the path on disk to the token for the given audience

func ReadLocal added in v0.2.0

func ReadLocal(audience string) (string, error)

ReadLocal reads the local machines token for the given audience

Suitable for passing as a bearer token

Types

type IssuerCallback

type IssuerCallback func(issuer string) error

An IssuerCallback is called whenever a token is verified to ensure it matches some expected criteria.

func AllowHerokuHost

func AllowHerokuHost(herokuHost string) IssuerCallback

AllowHerokuHost verifies that the issuer is from Heroku for the given host domain

func AllowHerokuSpace added in v0.2.0

func AllowHerokuSpace(herokuHost string, spaceIDs ...string) IssuerCallback

AllowHerokuSpace verifies that the issuer is from Heroku for the given host and space id.

type MalformedTokenError added in v0.2.0

type MalformedTokenError struct {
	// contains filtered or unexported fields
}

Returned when the token doesn't match the expected format

func (*MalformedTokenError) Error added in v0.2.0

func (e *MalformedTokenError) Error() string

func (*MalformedTokenError) Unwrap added in v0.2.0

func (e *MalformedTokenError) Unwrap() error

type Subject added in v0.2.0

type Subject struct {
	AppID   string `json:"app_id"`
	AppName string `json:"app_name"`
	Dyno    string `json:"dyno"`
}

Subject contains information about the app and dyno the token was issued for

func (*Subject) LogValue added in v0.2.0

func (s *Subject) LogValue() slog.Value

func (*Subject) MarshalText added in v0.2.0

func (s *Subject) MarshalText() ([]byte, error)

func (*Subject) String added in v0.2.0

func (s *Subject) String() string

func (*Subject) UnmarshalText added in v0.2.0

func (s *Subject) UnmarshalText(text []byte) error

type Token added in v0.2.0

type Token struct {
	IDToken *oidc.IDToken `json:"-"`
	SpaceID string        `json:"space_id"`
	Subject *Subject      `json:"subject"`
}

Token contains all of the token information stored by Heroku when it's issued

func FromContext added in v0.4.0

func FromContext(ctx context.Context) (*Token, error)

FromContext returns the Token or error associated with the given Context

func ReadLocalToken added in v0.2.0

func ReadLocalToken(ctx context.Context, audience string) (*Token, error)

ReadLocalToken reads the local machines token for the given audience and parses it

func (*Token) LogValue added in v0.2.0

func (t *Token) LogValue() slog.Value

type UntrustedIssuerError added in v0.2.0

type UntrustedIssuerError struct {
	Issuer string
}

Returned by an IssuerCallback getting an issuer it doesn't trust

func (*UntrustedIssuerError) Error added in v0.2.0

func (e *UntrustedIssuerError) Error() string

type Verifier

type Verifier struct {
	IssuerCallback IssuerCallback
	// contains filtered or unexported fields
}

A Verifier verifies a raw token with it's oids issuer and uses the IssuerCallback to ensure it's from a trusted source.

Example
package main

import (
	"fmt"

	"github.com/heroku/x/dynoid"
	"github.com/heroku/x/dynoid/internal"
)

const Audience = "testing"

func main() {
	// Normally a token would be passed in, but for testing we'll generate one
	ctx, token := internal.GenerateToken(Audience)

	verifier := dynoid.New(Audience)
	verifier.IssuerCallback = dynoid.AllowHerokuHost("heroku.local") // heroku.com for production

	t, err := verifier.Verify(ctx, token)
	if err != nil {
		fmt.Printf("failed to verify token (%v)", err)
		return
	}

	fmt.Println(t.Subject.AppID)
	fmt.Println(t.Subject.AppName)
	fmt.Println(t.Subject.Dyno)
}
Output:

00000000-0000-0000-0000-000000000001
sushi
web.1

func New

func New(audience string) *Verifier

Instantiate a new Verifier without an IssuerCallback set.

The IssuerCallback must be set before calling Verify or an error will be returned.

func NewWithCallback added in v0.2.0

func NewWithCallback(audience string, callback IssuerCallback) *Verifier

Instantiate a new Verifier with the IssuerCallback set.

func (*Verifier) Verify

func (v *Verifier) Verify(ctx context.Context, rawIDToken string) (*Token, error)

Verify validates the given token with the OIDC provider and validates it against the IssuerCallback

Directories

Path Synopsis
dynoidtest provides helper functions for testing code that uses DynoID
dynoidtest provides helper functions for testing code that uses DynoID

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL