[!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(ctx context.Context, err error) context.Context
ContextWithError adds the given error to the Context to be retrieved later by calling FromContext
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(audience string) string
LocalTokenPath returns the path on disk to the token for the given audience
func ReadLocal(audience string) (string, error)
ReadLocal reads the local machines token for the given audience
Suitable for passing as a bearer token
An IssuerCallback is called whenever a token is verified to ensure it matches some expected criteria.
type IssuerCallback func(issuer string) error
func AllowHerokuHost(herokuHost string) IssuerCallback
AllowHerokuHost verifies that the issuer is from Heroku for the given host domain
func AllowHerokuSpace(herokuHost string, spaceIDs ...string) IssuerCallback
AllowHerokuSpace verifies that the issuer is from Heroku for the given host and space id.
Returned when the token doesn't match the expected format
type MalformedTokenError struct {
// contains filtered or unexported fields
}
func (e *MalformedTokenError) Error() string
func (e *MalformedTokenError) Unwrap() error
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 (s *Subject) LogValue() slog.Value
func (s *Subject) MarshalText() ([]byte, error)
func (*Subject) String
func (s *Subject) String() string
func (s *Subject) UnmarshalText(text []byte) error
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(ctx context.Context) (*Token, error)
FromContext returns the Token or error associated with the given Context
func ReadLocalToken(ctx context.Context, audience string) (*Token, error)
ReadLocalToken reads the local machines token for the given audience and parses it
func (t *Token) LogValue() slog.Value
Returned by an IssuerCallback getting an issuer it doesn't trust
type UntrustedIssuerError struct {
Issuer string
}
func (*UntrustedIssuerError) Error
func (e *UntrustedIssuerError) Error() string
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(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(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(iss *Issuer, audiences ...string) error
Configure dynoid.DefaultFS to use tokens generated by the provided issuer for the audiences listed.
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(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(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 (f *FS) ReadFile(name string) ([]byte, error)
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(opts ...IssuerOpt) (*Issuer, error)
Create a new Issuer with the supplied opts applied
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 (iss *Issuer) GenerateIDToken(audience string, opts ...TokenOpt) (string, error)
GenerateIDToken returns a new signed token as a string
func (iss *Issuer) HTTPClient() *http.Client
HTTPClient returns a client that leverages the Issuer to validate tokens.
IssuerOpt allows the behavior of the issuer to be modified.
type IssuerOpt interface {
// contains filtered or unexported methods
}
func WithHerokuHost(herokuHost string) IssuerOpt
WithHerokuHost allows an issuer host to be supplied instead of using the default
func WithKey(key *rsa.PrivateKey) IssuerOpt
WithKey allows you to set the issuer's private key. Useful for leveraging test middleware.
func WithSpaceID(spaceID string) IssuerOpt
WithSpaceID allows a spaceID to be supplied instead of using the default
func WithTokenOpts(opts ...TokenOpt) IssuerOpt
WithTokenOpts allows a default set of TokenOpt to be applied to every token generated by the issuer
LocalConfiguration provides methods for working with a local issuer configured with ConfigureLocal
type LocalConfiguration struct {
// contains filtered or unexported fields
}
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(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.
A TokenOpt modifies the way a token is minted
type TokenOpt interface {
// contains filtered or unexported methods
}
func WithSubject(s *dynoid.Subject) TokenOpt
WithSubject allows the Subject to be different than the default
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(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(audience string, callback dynoid.IssuerCallback) func(http.Handler) http.Handler
Authorize populates the dyno identity blocks requests where the callback fails.
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(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(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(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