Skip to content

Commit

Permalink
Merge pull request #272 from xmidt-org/feature/remove-credentials
Browse files Browse the repository at this point in the history
Feature/remove credentials
  • Loading branch information
johnabass authored Aug 6, 2024
2 parents 5a5aa08 + 4bcfc4e commit 74c4ff5
Show file tree
Hide file tree
Showing 24 changed files with 610 additions and 885 deletions.
4 changes: 2 additions & 2 deletions authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ type Authorizers[R any] []Authorizer[R]
// Append tacks on one or more authorizers to this collection. The possibly
// new Authorizers instance is returned. The semantics of this method are
// the same as the built-in append.
func (as Authorizers[R]) Append(a ...Authorizer[R]) Authorizers[R] {
return append(as, a...)
func (as Authorizers[R]) Append(more ...Authorizer[R]) Authorizers[R] {
return append(as, more...)
}

// Authorize requires all authorizers in this sequence to allow access. This
Expand Down
128 changes: 128 additions & 0 deletions basculehttp/authorization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package basculehttp

import (
"context"
"errors"
"net/http"
"strings"

"github.com/xmidt-org/bascule/v1"
)

const (
// DefaultAuthorizationHeader is the default HTTP header used for authorization
// tokens in an HTTP request.
DefaultAuthorizationHeader = "Authorization"
)

var (
// ErrInvalidAuthorization indicates an authorization header value did not
// correspond to the standard.
ErrInvalidAuthorization = errors.New("invalidation authorization")

// ErrMissingAuthorization indicates that no authorization header was
// present in the source HTTP request.
ErrMissingAuthorization = errors.New("missing authorization")
)

// fastIsSpace tests an ASCII byte to see if it's whitespace.
// HTTP headers are restricted to US-ASCII, so we don't need
// the full unicode stack.
func fastIsSpace(b byte) bool {
return b == ' ' || b == '\t' || b == '\n' || b == '\r' || b == '\v' || b == '\f'
}

// ParseAuthorization parses an authorization value typically passed in
// the Authorization HTTP header.
//
// The required format is <scheme><single space><credential value>. This function
// is strict: it requires no leading or trailing space and exactly (1) space as
// a separator. If the raw value does not adhere to this format, ErrInvalidAuthorization
// is returned.
func ParseAuthorization(raw string) (s Scheme, v string, err error) {
var scheme string
var found bool
scheme, v, found = strings.Cut(raw, " ")
if found && len(scheme) > 0 && !fastIsSpace(v[0]) && !fastIsSpace(v[len(v)-1]) {
s = Scheme(scheme)
} else {
err = ErrInvalidAuthorization
}

return
}

type AuthorizationParserOption interface {
apply(*AuthorizationParser) error
}

type authorizationParserOptionFunc func(*AuthorizationParser) error

func (apof authorizationParserOptionFunc) apply(ap *AuthorizationParser) error { return apof(ap) }

func WithAuthorizationHeader(header string) AuthorizationParserOption {
return authorizationParserOptionFunc(func(ap *AuthorizationParser) error {
ap.header = header
return nil
})
}

func WithScheme(scheme Scheme, parser bascule.TokenParser[string]) AuthorizationParserOption {
return authorizationParserOptionFunc(func(ap *AuthorizationParser) error {
// we want case-insensitive matches, so lowercase everything
ap.parsers[scheme.lower()] = parser
return nil
})
}

type AuthorizationParser struct {
header string
parsers map[Scheme]bascule.TokenParser[string]
}

func NewAuthorizationParser(opts ...AuthorizationParserOption) (*AuthorizationParser, error) {
ap := &AuthorizationParser{
parsers: make(map[Scheme]bascule.TokenParser[string]),
}

for _, o := range opts {
if err := o.apply(ap); err != nil {
return nil, err
}
}

if len(ap.header) == 0 {
ap.header = DefaultAuthorizationHeader
}

return ap, nil
}

// Parse extracts the appropriate header, Authorization by default, and parses the
// scheme and value. Schemes are case-insensitive, e.g. BASIC and Basic are the same scheme.
//
// If a token parser is registered for the given scheme, that token parser is invoked.
// Otherwise, UnsupportedSchemeError is returned, indicating the scheme in question.
func (ap *AuthorizationParser) Parse(ctx context.Context, source *http.Request) (bascule.Token, error) {
authValue := source.Header.Get(ap.header)
if len(authValue) == 0 {
return nil, bascule.ErrMissingCredentials
}

scheme, value, err := ParseAuthorization(authValue)
if err != nil {
return nil, err
}

p, registered := ap.parsers[scheme.lower()]
if !registered {
return nil, &UnsupportedSchemeError{
Scheme: scheme,
}
}

return p.Parse(ctx, value)
}
73 changes: 40 additions & 33 deletions basculehttp/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,55 +6,62 @@ package basculehttp
import (
"context"
"encoding/base64"
"net/http"
"strings"

"github.com/xmidt-org/bascule/v1"
)

// InvalidBasicAuthError indicates that the Basic credentials were improperly
// encoded, either due to base64 issues or formatting.
type InvalidBasicAuthError struct {
// Cause represents the lower level error that occurred, e.g. a base64
// encoding error.
Cause error
// BasicToken is the interface that Basic Auth tokens implement.
type BasicToken interface {
UserName() string
Password() string
}

func (err *InvalidBasicAuthError) Unwrap() error { return err.Cause }
// basicToken is the internal basic token struct that results from
// parsing a Basic Authorization header value.
type basicToken struct {
userName string
password string
}

func (err *InvalidBasicAuthError) Error() string {
var o strings.Builder
o.WriteString("Basic auth string not encoded properly")
func (bt basicToken) Principal() string {
return bt.userName
}

if err.Cause != nil {
o.WriteString(": ")
o.WriteString(err.Cause.Error())
}
func (bt basicToken) UserName() string {
return bt.userName
}

return o.String()
func (bt basicToken) Password() string {
return bt.password
}

type basicTokenParser struct{}
// BasicTokenParser is a string-based bascule.TokenParser that produces
// BasicToken instances from strings.
//
// An instance of this parser may be passed to WithScheme in order to
// configure an AuthorizationParser.
type BasicTokenParser struct{}

func (btp basicTokenParser) Parse(_ context.Context, _ *http.Request, c bascule.Credentials) (t bascule.Token, err error) {
var decoded []byte
decoded, err = base64.StdEncoding.DecodeString(c.Value)
// Parse assumes that value is of the format required by https://datatracker.ietf.org/doc/html/rfc7617.
// The returned Token will return the basic auth username from its Principal() method.
// The returned Token will also implement BasicToken.
func (BasicTokenParser) Parse(_ context.Context, value string) (bascule.Token, error) {
// this mimics what the stdlib does at net/http.Request.BasicAuth()
raw, err := base64.StdEncoding.DecodeString(value)
if err != nil {
err = &InvalidBasicAuthError{
Cause: err,
}

return
return nil, bascule.ErrInvalidCredentials
}

username, _, found := strings.Cut(string(decoded), ":")
if found {
t = &Token{
principal: username,
}
} else {
err = &InvalidBasicAuthError{}
var (
bt basicToken
ok bool
)

bt.userName, bt.password, ok = strings.Cut(string(raw), ":")
if ok {
return bt, nil
}

return
return nil, bascule.ErrInvalidCredentials
}
16 changes: 4 additions & 12 deletions basculehttp/challenge.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,9 @@ package basculehttp
import (
"net/http"
"strings"

"github.com/xmidt-org/bascule/v1"
)

const (
// BasicScheme is the name of the basic HTTP authentication scheme.
BasicScheme bascule.Scheme = "Basic"

// BearerScheme is the name of the bearer HTTP authentication scheme.
BearerScheme bascule.Scheme = "Bearer"

// WwwAuthenticateHeaderName is the HTTP header used for StatusUnauthorized challenges.
WwwAuthenticateHeaderName = "WWW-Authenticate"

Expand Down Expand Up @@ -63,7 +55,7 @@ func (chs Challenges) WriteHeader(h http.Header) {
type BasicChallenge struct {
// Scheme is the name of scheme supplied in the challenge. If this
// field is unset, BasicScheme is used.
Scheme bascule.Scheme
Scheme Scheme

// Realm is the name of the realm for the challenge. If this field
// is unset, DefaultBasicRealm is used.
Expand All @@ -81,7 +73,7 @@ func (bc BasicChallenge) FormatAuthenticate(o strings.Builder) {
if len(bc.Scheme) > 0 {
o.WriteString(string(bc.Scheme))
} else {
o.WriteString(string(BasicScheme))
o.WriteString(string(SchemeBasic))
}

o.WriteString(` realm="`)
Expand All @@ -100,7 +92,7 @@ func (bc BasicChallenge) FormatAuthenticate(o strings.Builder) {
type BearerChallenge struct {
// Scheme is the name of scheme supplied in the challenge. If this
// field is unset, BearerScheme is used.
Scheme bascule.Scheme
Scheme Scheme

// Realm is the name of the realm for the challenge. If this field
// is unset, DefaultBearerRealm is used.
Expand All @@ -114,7 +106,7 @@ func (bc BearerChallenge) FormatAuthenticate(o strings.Builder) {
if len(bc.Scheme) > 0 {
o.WriteString(string(bc.Scheme))
} else {
o.WriteString(string(BasicScheme))
o.WriteString(string(SchemeBearer))
}

o.WriteString(` realm="`)
Expand Down
Loading

0 comments on commit 74c4ff5

Please sign in to comment.