Skip to content

Commit

Permalink
feat: cached
Browse files Browse the repository at this point in the history
  • Loading branch information
james-d-elliott committed Jun 9, 2024
1 parent b7b6c65 commit d0dc6fc
Show file tree
Hide file tree
Showing 9 changed files with 350 additions and 18 deletions.
2 changes: 1 addition & 1 deletion metadata/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func Fetch() (metadata *Metadata, err error) {
res *http.Response
)

if decoder, err = NewDecoder(); err != nil {
if decoder, err = NewDecoder(WithIgnoreEntryParsingErrors()); err != nil {
return nil, err
}

Expand Down
5 changes: 5 additions & 0 deletions metadata/providers/cached/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package cached

// The cached package handles a metadata.Provider implementation that both downloads and caches the MDS3 blob. This
// effectively is the recommended provider in most instances as it's fairly robust. Alternatively we suggest implementing
// a similar provider that leverages the memory.Provider as an underlying element.
92 changes: 92 additions & 0 deletions metadata/providers/cached/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package cached

import (
"net/http"
"net/url"

"github.com/go-webauthn/webauthn/metadata"
)

// Option describes an optional pattern for this provider.
type Option func(provider *Provider) (err error)

// NewFunc describes the type used to create the underlying provider.
type NewFunc func(mds *metadata.Metadata) (provider metadata.Provider, err error)

// WithPath sets the path name for the cached file. This option is REQUIRED.
func WithPath(name string) Option {
return func(provider *Provider) (err error) {
provider.name = name

return nil
}
}

// WithUpdate is used to enable or disable the update. By default it's set to true.
func WithUpdate(update bool) Option {
return func(provider *Provider) (err error) {
provider.update = update

return nil
}
}

// WithForceUpdate is used to force an update on creation. This will forcibly overwrite the file if possible.
func WithForceUpdate(force bool) Option {
return func(provider *Provider) (err error) {
provider.force = force

return nil
}
}

// WithNew customizes the NewFunc. By default we just create a fairly standard memory.Provider with strict defaults.
func WithNew(newup NewFunc) Option {
return func(provider *Provider) (err error) {
provider.newup = newup

return nil
}
}

// WithDecoder sets the decoder to be used for this provider. By default this is a decoder with the entry parsing errors
// configured to skip that entry.
func WithDecoder(decoder *metadata.Decoder) Option {
return func(provider *Provider) (err error) {
provider.decoder = decoder

return nil
}
}

// WithMetadataURL configures the URL to get the metadata from. This shouldn't be modified unless you know what you're
// doing as we use the metadata.ProductionMDSURL which is safe in most instances.
func WithMetadataURL(uri string) Option {
return func(provider *Provider) (err error) {
if _, err = url.ParseRequestURI(uri); err != nil {
return err
}

provider.uri = uri

return nil
}
}

// WithClient configures the *http.Client used to get the MDS3 blob.
func WithClient(client *http.Client) Option {
return func(provider *Provider) (err error) {
provider.client = client

return nil
}
}

// WithClock allows injection of a metadata.Clock to check the up-to-date status of a blob.
func WithClock(clock metadata.Clock) Option {
return func(provider *Provider) (err error) {
provider.clock = clock

return nil
}
}
146 changes: 146 additions & 0 deletions metadata/providers/cached/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package cached

import (
"fmt"
"io"
"net/http"
"os"

"github.com/go-webauthn/webauthn/metadata"
)

// New returns a new cached Provider given a set of functional Option's. This provider will download a new version and
// save it to the configured file path if it doesn't exist or if it's out of date by default.
func New(opts ...Option) (provider metadata.Provider, err error) {
p := &Provider{
update: true,
uri: metadata.ProductionMDSURL,
}

for _, opt := range opts {
if err = opt(p); err != nil {
return nil, err
}
}

if p.name == "" {
return nil, fmt.Errorf("provider configured without setting a path for the cached file blob")
}

if p.newup == nil {
p.newup = defaultNew
}

if p.decoder == nil {
if p.decoder, err = metadata.NewDecoder(metadata.WithIgnoreEntryParsingErrors()); err != nil {
return nil, err
}
}

if p.clock == nil {
p.clock = &metadata.RealClock{}
}

if err = p.init(); err != nil {
return nil, err
}

return p, nil
}

// Provider implements a metadata.Provider with a file-based cache.
type Provider struct {
metadata.Provider

name string
uri string
update bool
force bool
clock metadata.Clock
client *http.Client
decoder *metadata.Decoder
newup NewFunc
}

func (p *Provider) init() (err error) {
var (
f *os.File
rc io.ReadCloser
created bool
mds *metadata.Metadata
)

if f, created, err = doOpenOrCreate(p.name); err != nil {
return err
}

defer f.Close()

if created || p.force {
if rc, err = p.get(); err != nil {
return err
}
} else {
if mds, err = p.parse(f); err != nil {
return err
}

if p.outdated(mds) {
if rc, err = p.get(); err != nil {
return err
}
}
}

if rc != nil {
if err = doTruncateCopyAndSeekStart(f, rc); err != nil {
return err
}

if mds, err = p.parse(f); err != nil {
return err
}
}

var provider metadata.Provider

if provider, err = p.newup(mds); err != nil {
return err
}

p.Provider = provider

return nil
}

func (p *Provider) parse(rc io.ReadCloser) (data *metadata.Metadata, err error) {
var payload *metadata.MetadataBLOBPayloadJSON

if payload, err = p.decoder.Decode(rc); err != nil {
return nil, err
}

if data, err = p.decoder.Parse(payload); err != nil {
return nil, err
}

return data, nil
}

func (p *Provider) outdated(mds *metadata.Metadata) bool {
return p.update && p.clock.Now().After(mds.Parsed.NextUpdate)
}

func (p *Provider) get() (f io.ReadCloser, err error) {
if p.client == nil {
p.client = &http.Client{}
}

var res *http.Response

if res, err = p.client.Get(p.uri); err != nil {
return nil, err
}

return res.Body, nil
}
50 changes: 50 additions & 0 deletions metadata/providers/cached/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cached

import (
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/metadata/providers/memory"
"io"
"os"
)

func doTruncateCopyAndSeekStart(f *os.File, rc io.ReadCloser) (err error) {
if err = f.Truncate(0); err != nil {
return err
}

if _, err = io.Copy(f, rc); err != nil {
return err
}

if _, err = f.Seek(0, io.SeekStart); err != nil {
return err
}

return rc.Close()
}

func doOpenOrCreate(name string) (f *os.File, created bool, err error) {
if f, err = os.Open(name); err == nil {
return f, false, nil
}

if os.IsNotExist(err) {
if f, err = os.Create(name); err != nil {
return nil, false, err
}

return f, true, nil
}

return nil, false, err
}

func defaultNew(mds *metadata.Metadata) (provider metadata.Provider, err error) {
return memory.New(
memory.WithMetadata(mds.ToMap()),
memory.WithValidateEntry(true),
memory.WithValidateEntryPermitZeroAAGUID(false),
memory.WithValidateTrustAnchor(true),
memory.WithValidateStatus(true),
)
}
5 changes: 5 additions & 0 deletions metadata/providers/memory/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package memory

// The memory package handles a metadata.Provider implementation that solely exists in memory. It's intended as a basis
// for other providers and generally not recommended to use directly unless you're implementing your own logic to handle
// the download and potential caching of the MDS3 blob yourself.
Original file line number Diff line number Diff line change
Expand Up @@ -7,60 +7,74 @@ import (
)

// Option describes an optional pattern for this provider.
type Option func(*Provider)
type Option func(provider *Provider) (err error)

// WithMetadata provides the required metadata for the memory provider.
func WithMetadata(metadata map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry) Option {
return func(provider *Provider) {
provider.mds = metadata
func WithMetadata(mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry) Option {
return func(provider *Provider) (err error) {
provider.mds = mds

return nil
}
}

// WithValidateEntry requires that the provided metadata has an entry for the given authenticator to be considered
// valid. By default an AAGUID which has a zero value should fail validation if WithValidateEntryPermitZeroAAGUID is not
// provided with the value of true.
func WithValidateEntry(require bool) Option {
return func(provider *Provider) {
return func(provider *Provider) (err error) {
provider.entry = require

return nil
}
}

// WithValidateEntryPermitZeroAAGUID is an option that permits a zero'd AAGUID from an attestation statement to
// automatically pass metadata validations. Generally helpful to use with WithValidateEntry.
func WithValidateEntryPermitZeroAAGUID(permit bool) Option {
return func(provider *Provider) {
return func(provider *Provider) (err error) {
provider.entryPermitZero = permit

return nil
}
}

// WithValidateTrustAnchor when set to true enables the validation of the attestation statement against the trust anchor
// from the metadata.
func WithValidateTrustAnchor(validate bool) Option {
return func(provider *Provider) {
return func(provider *Provider) (err error) {
provider.anchors = validate

return nil
}
}

// WithValidateStatus when set to true enables the validation of the attestation statments AAGUID against the desired
// and undesired metadata.AuthenticatorStatus lists.
func WithValidateStatus(validate bool) Option {
return func(provider *Provider) {
return func(provider *Provider) (err error) {
provider.status = validate

return nil
}
}

// WithStatusUndesired provides the list of statuses which are considered undesirable for status report validation
// purposes. Should be used with WithValidateStatus set to true.
func WithStatusUndesired(statuses []metadata.AuthenticatorStatus) Option {
return func(provider *Provider) {
return func(provider *Provider) (err error) {
provider.undesired = statuses

return nil
}
}

// WithStatusDesired provides the list of statuses which are considered desired and will be required for status report
// validation purposes. Should be used with WithValidateStatus set to true.
func WithStatusDesired(statuses []metadata.AuthenticatorStatus) Option {
return func(provider *Provider) {
return func(provider *Provider) (err error) {
provider.desired = statuses

return nil
}
}
Loading

0 comments on commit d0dc6fc

Please sign in to comment.