diff --git a/.reuse/dep5 b/.reuse/dep5 index 8436f4f..e8a496c 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -67,3 +67,10 @@ Files: examples/basculehttp/go.sum Copyright: SPDX-FileCopyrightText: 2019 Comcast Cable Communications Management, LLC License: Apache-2.0 +Files: v2/go.mod +Copyright: SPDX-FileCopyrightText: 2019 Comcast Cable Communications Management, LLC +License: Apache-2.0 + +Files: v2/go.sum +Copyright: SPDX-FileCopyrightText: 2019 Comcast Cable Communications Management, LLC +License: Apache-2.0 diff --git a/v2/authenticator.go b/v2/authenticator.go new file mode 100644 index 0000000..e7b5a2c --- /dev/null +++ b/v2/authenticator.go @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package bascule + +import "go.uber.org/multierr" + +// Authenticator provides a strategy for verifying that a Token +// is valid beyond just simple parsing. For example, an Authenticator may +// verify certain roles or capabilities. +type Authenticator interface { + // Authenticate verifies the given Token. + Authenticate(Token) error +} + +// Authenticators is an aggregate Authenticator. +type Authenticators []Authenticator + +// Authenticate applies each contained Authenticator in order. All Authenticators +// are executed. The returned error, if not nil, will be an aggregate of all errors +// that occurred. +func (as Authenticators) Authenticate(t Token) (err error) { + for _, auth := range as { + err = multierr.Append(err, auth.Authenticate(t)) + } + + return +} diff --git a/v2/authorizer.go b/v2/authorizer.go new file mode 100644 index 0000000..f342e15 --- /dev/null +++ b/v2/authorizer.go @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package bascule + +// Authorizer is a strategy for verifying that a given Token has +// access to resources. +type Authorizer interface { + // Authorize verifies that the given Token can access a resource. + Authorize(resource any, t Token) error +} diff --git a/v2/basculehttp/accessor.go b/v2/basculehttp/accessor.go new file mode 100644 index 0000000..0a18264 --- /dev/null +++ b/v2/basculehttp/accessor.go @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "net/http" + "strings" + + "github.com/xmidt-org/bascule/v2" +) + +const DefaultAuthorizationHeader = "Authorization" + +// Accessor is the strategy for extracting the raw, serialized credentials +// from an HTTP request. +type Accessor interface { + // GetCredentials obtains the raw, serialized credentials from the request. + GetCredentials(*http.Request) (string, error) +} + +var defaultAccessor Accessor = AuthorizationAccessor{} + +func DefaultAccessor() Accessor { return defaultAccessor } + +// AuthorizationAccessor is an Accessor that pulls the serialized credentials +// from an HTTP header of the format defined by https://www.rfc-editor.org/rfc/rfc7235#section-4.2. +// Only the single header is considered. +type AuthorizationAccessor struct { + // Header is the name of the Authorization header. If unset, then + // DefaultAuthorizationHeader is used. + Header string +} + +func (aa AuthorizationAccessor) header() string { + if len(aa.Header) == 0 { + return DefaultAuthorizationHeader + } + + return aa.Header +} + +func (aa AuthorizationAccessor) GetCredentials(r *http.Request) (serialized string, err error) { + header := aa.header() + serialized = r.Header.Get(header) + + if len(serialized) == 0 { + var reason strings.Builder + reason.WriteString("missing header ") + reason.WriteString(header) + err = &bascule.MissingCredentialsError{ + Reason: reason.String(), + } + } + + return +} diff --git a/v2/basculehttp/basicToken.go b/v2/basculehttp/basicToken.go new file mode 100644 index 0000000..c34c231 --- /dev/null +++ b/v2/basculehttp/basicToken.go @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "encoding/base64" + "strings" + + "github.com/xmidt-org/bascule/v2" +) + +type basicToken struct { + credentials bascule.Credentials + userName string +} + +func (bt *basicToken) Credentials() bascule.Credentials { return bt.credentials } + +func (bt *basicToken) Principal() string { return bt.userName } + +type basicTokenParser struct{} + +func (basicTokenParser) Parse(c bascule.Credentials) (t bascule.Token, err error) { + var decoded []byte + decoded, err = base64.StdEncoding.DecodeString(c.Value) + if err == nil { + if userName, _, ok := strings.Cut(string(decoded), ":"); ok { + t = &basicToken{ + credentials: c, + userName: userName, + } + } else { + err = &bascule.InvalidCredentialsError{ + Raw: c.Value, + } + } + } + + return +} diff --git a/v2/basculehttp/challenge.go b/v2/basculehttp/challenge.go new file mode 100644 index 0000000..d788db0 --- /dev/null +++ b/v2/basculehttp/challenge.go @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "net/http" + "strings" + + "github.com/xmidt-org/bascule/v2" +) + +const ( + BasicScheme bascule.Scheme = "Basic" + BearerScheme bascule.Scheme = "Bearer" + + // WwwAuthenticateHeaderName is the HTTP header used for StatusUnauthorized challenges. + WwwAuthenticateHeaderName = "WWW-Authenticate" + + // DefaultBasicRealm is the realm used for a basic challenge + // when no realm is supplied. + DefaultBasicRealm string = "bascule" + + // DefaultBearerRealm is the realm used for a bearer challenge + // when no realm is supplied. + DefaultBearerRealm string = "bascule" +) + +// Challenge represents a WWW-Authenticate challenge. +type Challenge interface { + // FormatAuthenticate formats the authenticate string. + FormatAuthenticate(strings.Builder) +} + +// Challenges represents a sequence of challenges to associated with +// a StatusUnauthorized response. +type Challenges []Challenge + +// WriteHeader inserts one WWW-Authenticate header per challenge in this set. +// If this set is empty, the given http.Header is not modified. +// +// This method returns the count of headers added, which will be zero (0) for +// an empty Challenges. +func (chs Challenges) WriteHeader(h http.Header) int { + var o strings.Builder + for _, ch := range chs { + ch.FormatAuthenticate(o) + h.Add(WwwAuthenticateHeaderName, o.String()) + o.Reset() + } + + return len(chs) +} + +// BasicChallenge represents a WWW-Authenticate basic auth challenge. +type BasicChallenge struct { + // Scheme is the name of scheme supplied in the challenge. If this + // field is unset, BasicScheme is used. + Scheme bascule.Scheme + + // Realm is the name of the realm for the challenge. If this field + // is unset, DefaultBasicRealm is used. + // + // Note that this field should always be set. The default isn't very + // useful outside of development. + Realm string + + // UTF8 indicates whether "charset=UTF-8" is appended to the challenge. + // This is the only charset allowed for a Basic challenge. + UTF8 bool +} + +func (bc BasicChallenge) FormatAuthenticate(o strings.Builder) { + if len(bc.Scheme) > 0 { + o.WriteString(string(bc.Scheme)) + } else { + o.WriteString(string(BasicScheme)) + } + + o.WriteString(` realm="`) + if len(bc.Realm) > 0 { + o.WriteString(bc.Realm) + } else { + o.WriteString(DefaultBasicRealm) + } + + o.WriteRune('"') + if bc.UTF8 { + o.WriteString(`, charset="UTF-8"`) + } +} + +type BearerChallenge struct { + // Scheme is the name of scheme supplied in the challenge. If this + // field is unset, BearerScheme is used. + Scheme bascule.Scheme + + // Realm is the name of the realm for the challenge. If this field + // is unset, DefaultBearerRealm is used. + // + // Note that this field should always be set. The default isn't very + // useful outside of development. + Realm string +} + +func (bc BearerChallenge) FormatAuthenticate(o strings.Builder) { + if len(bc.Scheme) > 0 { + o.WriteString(string(bc.Scheme)) + } else { + o.WriteString(string(BasicScheme)) + } + + o.WriteString(` realm="`) + if len(bc.Realm) > 0 { + o.WriteString(bc.Realm) + } else { + o.WriteString(DefaultBasicRealm) + } +} diff --git a/v2/basculehttp/frontDoor.go b/v2/basculehttp/frontDoor.go new file mode 100644 index 0000000..f803895 --- /dev/null +++ b/v2/basculehttp/frontDoor.go @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "errors" + "net/http" + + "github.com/xmidt-org/bascule/v2" + "go.uber.org/multierr" +) + +type FrontDoorOption interface { + apply(*frontDoor) error +} + +type frontDoorOptionFunc func(*frontDoor) error + +func (fdof frontDoorOptionFunc) apply(fd *frontDoor) error { return fdof(fd) } + +// WithAccessor associates a strategy for extracting the raw, serialized token +// from a request. If this option is not supplied, DefaultAccessor() is used. +func WithAccessor(a Accessor) FrontDoorOption { + return frontDoorOptionFunc(func(fd *frontDoor) error { + fd.accessor = a + return nil + }) +} + +// WithTokenFactory associates the given token factory with a front door. +func WithTokenFactory(tf bascule.TokenFactory) FrontDoorOption { + return frontDoorOptionFunc(func(fd *frontDoor) error { + fd.tokenFactory = tf + return nil + }) +} + +// WithChallenges describes challenges to be issued when no credentials +// are supplied. If no challenges are associated with a FrontDoor, then +// http.StatusForbidden is returned whenever credentials are not found in +// the request. Otherwise, http.StatusUnauthorized is returned along +// with a WWW-Authenticate header for each challenge. +func WithChallenges(c ...Challenge) FrontDoorOption { + return frontDoorOptionFunc(func(fd *frontDoor) error { + fd.challenges = append(fd.challenges, c...) + return nil + }) +} + +// FrontDoor is a server middleware that handles the full authentication workflow. +// Authorization is handled separately. +type FrontDoor interface { + Then(next http.Handler) http.Handler +} + +// NewFrontDoor constructs a FrontDoor middleware using the supplied options. +func NewFrontDoor(opts ...FrontDoorOption) (FrontDoor, error) { + fd := &frontDoor{ + accessor: DefaultAccessor(), + } + + var err error + for _, o := range opts { + err = multierr.Append(err, o.apply(fd)) + } + + if err != nil { + return nil, err + } + + return fd, nil +} + +type frontDoor struct { + challenges Challenges + forbidden func(http.ResponseWriter, *http.Request, error) // TODO + + accessor Accessor + tokenFactory bascule.TokenFactory +} + +func (fd *frontDoor) handleMissingCredentials(response http.ResponseWriter, err *bascule.MissingCredentialsError) { + var statusCode = http.StatusForbidden + if fd.challenges.WriteHeader(response.Header()) > 0 { + statusCode = http.StatusUnauthorized + } + + response.WriteHeader(statusCode) +} + +func (fd *frontDoor) handleInvalidCredentials(response http.ResponseWriter, err *bascule.InvalidCredentialsError) { + response.Header().Set("Content-Type", "text/plain") + response.WriteHeader(http.StatusBadRequest) + response.Write([]byte(err.Error())) +} + +func (fd *frontDoor) handleError(response http.ResponseWriter, request *http.Request, err error) { + { + var missing *bascule.MissingCredentialsError + if errors.As(err, &missing) { + fd.handleMissingCredentials(response, missing) + return + } + } + + { + var invalid *bascule.InvalidCredentialsError + if errors.As(err, &invalid) { + fd.handleInvalidCredentials(response, invalid) + return + } + } +} + +func (fd *frontDoor) Then(next http.Handler) http.Handler { + accessor := fd.accessor + if accessor == nil { + accessor = DefaultAccessor() + } + + return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { + var token bascule.Token + raw, err := accessor.GetCredentials(request) + if err == nil { + token, err = fd.tokenFactory.NewToken(raw) + } + + if err != nil { + fd.handleError(response, request, err) + return + } + + request = request.WithContext( + bascule.WithToken(request.Context(), token), + ) + + next.ServeHTTP(response, request) + }) +} diff --git a/v2/basculehttp/jwtToken.go b/v2/basculehttp/jwtToken.go new file mode 100644 index 0000000..958c37a --- /dev/null +++ b/v2/basculehttp/jwtToken.go @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/xmidt-org/bascule/v2" +) + +type jwtToken struct { + credentials bascule.Credentials + token jwt.Token +} + +func (jt *jwtToken) Credentials() bascule.Credentials { return jt.credentials } + +func (jt *jwtToken) Principal() string { return jt.token.Subject() } + +type jwtTokenParser struct { + options []jwt.ParseOption +} + +func (jtp jwtTokenParser) Parse(c bascule.Credentials) (t bascule.Token, err error) { + var token jwt.Token + token, err = jwt.Parse([]byte(c.Value), jtp.options...) + if err == nil { + t = &jwtToken{ + token: token, + } + } + + return +} + +func NewJwtTokenParser(opts ...jwt.ParseOption) (bascule.TokenParser, error) { + return &jwtTokenParser{ + options: append([]jwt.ParseOption{}, opts...), + }, nil +} diff --git a/v2/basculehttp/tokenFactory.go b/v2/basculehttp/tokenFactory.go new file mode 100644 index 0000000..5a1798d --- /dev/null +++ b/v2/basculehttp/tokenFactory.go @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "strings" + + "github.com/xmidt-org/bascule/v2" +) + +type defaultCredentialsParser struct{} + +func (defaultCredentialsParser) Parse(serialized string) (c bascule.Credentials, err error) { + parts := strings.Split(serialized, " ") + if len(parts) != 2 { + err = &bascule.InvalidCredentialsError{ + Raw: serialized, + } + } else { + c.Scheme = bascule.Scheme(parts[0]) + c.Value = parts[1] + } + + return +} + +// NewTokenFactory builds a bascule.TokenFactory with useful defaults for an +// HTTP environment. +// +// A default CredentialParser and TokenParser schemes are prepended to the supplied +// option. This function will not return an error if those options are omitted. +// Any options supplied explicitly to this function can override those defaults. +func NewTokenFactory(opts ...bascule.TokenFactoryOption) (bascule.TokenFactory, error) { + opts = append( + // prepend defaults, allowing subsequent options to override + []bascule.TokenFactoryOption{ + bascule.WithCredentialsParser(defaultCredentialsParser{}), + bascule.WithTokenParser(BasicScheme, basicTokenParser{}), + // TODO: add Bearer + }, + opts..., + ) + + return bascule.NewTokenFactory(opts...) +} diff --git a/v2/capabilities.go b/v2/capabilities.go new file mode 100644 index 0000000..48bcc44 --- /dev/null +++ b/v2/capabilities.go @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package bascule + +// GetCapabilities returns the set of security capabilities associated +// with the given Token. +// +// If the given Token has a Capabilities method that returns a []string, +// that method is used to determine the capabilities. Otherwise, this +// function returns an empty slice. +func GetCapabilities(t Token) (caps []string) { + type capabilities interface { + Capabilities() []string + } + + if c, ok := t.(capabilities); ok { + caps = c.Capabilities() + } + + return +} diff --git a/v2/context.go b/v2/context.go new file mode 100644 index 0000000..c8ea4d6 --- /dev/null +++ b/v2/context.go @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package bascule + +import "context" + +type tokenContextKey struct{} + +func GetToken[T Token](ctx context.Context, t *T) (found bool) { + *t, found = ctx.Value(tokenContextKey{}).(T) + return +} + +func WithToken[T Token](ctx context.Context, t T) context.Context { + return context.WithValue( + ctx, + tokenContextKey{}, + t, + ) +} diff --git a/v2/credentials.go b/v2/credentials.go new file mode 100644 index 0000000..6d5f6aa --- /dev/null +++ b/v2/credentials.go @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package bascule + +import "strings" + +// MissingCredentialsError indicates that credentials could not be found. +// Typically, this error will be returned by code that extracts credentials +// from some other source, e.g. an HTTP request. +type MissingCredentialsError struct { + // Cause represents the lower-level error that occurred, if any. + Cause error + + // Reason contains any additional information about the missing credentials. + Reason string +} + +func (err *MissingCredentialsError) Unwrap() error { return err.Cause } + +func (err *MissingCredentialsError) Error() string { + var o strings.Builder + o.WriteString("Missing credentials") + if len(err.Reason) > 0 { + o.WriteString(": ") + o.WriteString(err.Reason) + } + + return o.String() +} + +// InvalidCredentialsError is returned typically by CredentialsParser.Parse +// to indicate that a raw, serialized credentials were badly formatted. +type InvalidCredentialsError struct { + // Cause represents any lower-level error that occurred, if any. + Cause error + + // Raw represents the raw credentials that couldn't be parsed. + Raw string +} + +func (err *InvalidCredentialsError) Unwrap() error { return err.Cause } + +func (err *InvalidCredentialsError) Error() string { + var o strings.Builder + o.WriteString(`Invalid credentials: "`) + o.WriteString(err.Raw) + o.WriteString(`"`) + return o.String() +} + +// Scheme represents how a security token should be parsed. For HTTP, examples +// of a scheme are "Bearer" and "Basic". +type Scheme string + +// Credentials holds the raw, unparsed token information. +type Credentials struct { + // Scheme is the parsing scheme used for the credential value. + Scheme Scheme + + // Value is the raw, unparsed credential information. + Value string +} + +// CredentialsParser produces Credentials from their serialized form. +type CredentialsParser interface { + // Parse parses the raw, marshaled version of credentials and + // returns the Credentials object. + Parse(raw string) (Credentials, error) +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..280eb0c --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,23 @@ +module github.com/xmidt-org/bascule/v2 + +go 1.21 + +require ( + github.com/lestrrat-go/jwx/v2 v2.0.12 + go.uber.org/multierr v1.11.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/lestrrat-go/blackmagic v1.0.1 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.4 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/segmentio/asm v1.2.0 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/sys v0.12.0 // indirect +) diff --git a/v2/go.sum b/v2/go.sum new file mode 100644 index 0000000..7b1b676 --- /dev/null +++ b/v2/go.sum @@ -0,0 +1,83 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= +github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= +github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= +github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2/token.go b/v2/token.go new file mode 100644 index 0000000..d267071 --- /dev/null +++ b/v2/token.go @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package bascule + +import ( + "errors" +) + +// Token is a runtime representation of credentials. This interface will be further +// customized by infrastructure. +type Token interface { + // Credentials returns the raw, unparsed information used to produce this Token. + Credentials() Credentials + + // Principal is the security subject of this token, e.g. the user name or other + // user identifier. + Principal() string +} + +// TokenParser produces tokens from credentials. +type TokenParser interface { + // Parse turns a Credentials into a Token. This method may validate parts + // of the credential's value, but should not perform any authentication itself. + Parse(Credentials) (Token, error) +} + +// TokenParsers is a registry of parsers based on credential schemes. +// The zero value of this type is valid and ready to use. +type TokenParsers map[Scheme]TokenParser + +// Register adds or replaces the parser associated with the given scheme. +func (tp *TokenParsers) Register(scheme Scheme, p TokenParser) { + if *tp == nil { + *tp = make(TokenParsers) + } + + (*tp)[scheme] = p +} + +// Parse chooses a TokenParser based on the Scheme and invokes that +// parser. If the credential scheme is unsupported, an error is returned. +func (tp TokenParsers) Parse(c Credentials) (Token, error) { + p, ok := tp[c.Scheme] + if !ok { + return nil, errors.New("TODO: unsupported credential scheme error") + } + + return p.Parse(c) +} diff --git a/v2/tokenFactory.go b/v2/tokenFactory.go new file mode 100644 index 0000000..d72dcc3 --- /dev/null +++ b/v2/tokenFactory.go @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package bascule + +import ( + "errors" + + "go.uber.org/multierr" +) + +// TokenFactoryOption is a configurable option for building a TokenFactory. +type TokenFactoryOption interface { + apply(*tokenFactory) error +} + +type tokenFactoryOptionFunc func(*tokenFactory) error + +func (f tokenFactoryOptionFunc) apply(tf *tokenFactory) error { return f(tf) } + +// WithCredentialsParser establishes the strategy for parsing credentials for +// the TokenFactory being built. This option is required. +func WithCredentialsParser(cp CredentialsParser) TokenFactoryOption { + return tokenFactoryOptionFunc(func(tf *tokenFactory) error { + tf.credentialsParser = cp + return nil + }) +} + +// WithTokenParser registers a credential scheme with the TokenFactory. +// This option must be used at least once. +func WithTokenParser(scheme Scheme, tp TokenParser) TokenFactoryOption { + return tokenFactoryOptionFunc(func(tf *tokenFactory) error { + tf.tokenParsers.Register(scheme, tp) + return nil + }) +} + +// WithAuthenticators adds Authenticator rules to be used by the TokenFactory. +// Authenticator rules are optional. If omitted, then the TokenFactory will +// not perform authentication. +func WithAuthenticators(auth ...Authenticator) TokenFactoryOption { + return tokenFactoryOptionFunc(func(tf *tokenFactory) error { + tf.authenticators = append(tf.authenticators, auth...) + return nil + }) +} + +// TokenFactory brings together the entire authentication workflow. For typical +// code that uses bascule, this is the primary interface for obtaining Tokens. +type TokenFactory interface { + // NewToken accepts a raw, serialized set of credentials and turns it + // into a Token. This method executes the workflow of: + // + // - parsing the serialized credentials into a Credentials + // - parsing the Credentials into a Token + // - executing any Authenticator rules against the Token + NewToken(serialized string) (Token, error) +} + +// NewTokenFactory creates a TokenFactory using the supplied option. +// +// A CredentialParser and at least one (1) TokenParser is required. If +// either are not supplied, this function returns an error. +func NewTokenFactory(opts ...TokenFactoryOption) (TokenFactory, error) { + tf := &tokenFactory{} + + var err error + for _, o := range opts { + err = multierr.Append(err, o.apply(tf)) + } + + if tf.credentialsParser == nil { + err = multierr.Append(err, errors.New("A CredentialsParser is required")) + } + + if len(tf.tokenParsers) == 0 { + err = multierr.Append(err, errors.New("At least one (1) TokenParser is required")) + } + + return tf, err +} + +// tokenFactory is the internal implementation of TokenFactory. +type tokenFactory struct { + credentialsParser CredentialsParser + tokenParsers TokenParsers + authenticators Authenticators +} + +func (tf *tokenFactory) NewToken(serialized string) (t Token, err error) { + var c Credentials + c, err = tf.credentialsParser.Parse(serialized) + if err == nil { + t, err = tf.tokenParsers.Parse(c) + } + + if err == nil { + err = tf.authenticators.Authenticate(t) + } + + return +}