Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/remove credentials #272

Merged
merged 6 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

Check warning on line 52 in basculehttp/authorization.go

View check run for this annotation

Codecov / codecov/patch

basculehttp/authorization.go#L52

Added line #L52 was not covered by tests
}

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
})

Check warning on line 70 in basculehttp/authorization.go

View check run for this annotation

Codecov / codecov/patch

basculehttp/authorization.go#L66-L70

Added lines #L66 - L70 were not covered by tests
}

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

Check warning on line 93 in basculehttp/authorization.go

View check run for this annotation

Codecov / codecov/patch

basculehttp/authorization.go#L93

Added line #L93 was not covered by tests
}
}

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

Check warning on line 117 in basculehttp/authorization.go

View check run for this annotation

Codecov / codecov/patch

basculehttp/authorization.go#L117

Added line #L117 was not covered by tests
}

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

Check warning on line 123 in basculehttp/authorization.go

View check run for this annotation

Codecov / codecov/patch

basculehttp/authorization.go#L122-L123

Added lines #L122 - L123 were not covered by tests
}
}

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 @@
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

Check warning on line 32 in basculehttp/basic.go

View check run for this annotation

Codecov / codecov/patch

basculehttp/basic.go#L31-L32

Added lines #L31 - L32 were not covered by tests
}

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

Check warning on line 36 in basculehttp/basic.go

View check run for this annotation

Codecov / codecov/patch

basculehttp/basic.go#L35-L36

Added lines #L35 - L36 were not covered by tests
}

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

Check warning on line 53 in basculehttp/basic.go

View check run for this annotation

Codecov / codecov/patch

basculehttp/basic.go#L53

Added line #L53 was not covered by tests
}

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

Check warning on line 66 in basculehttp/basic.go

View check run for this annotation

Codecov / codecov/patch

basculehttp/basic.go#L66

Added line #L66 was not covered by tests
}
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 @@
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 @@
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 @@
if len(bc.Scheme) > 0 {
o.WriteString(string(bc.Scheme))
} else {
o.WriteString(string(BasicScheme))
o.WriteString(string(SchemeBasic))

Check warning on line 76 in basculehttp/challenge.go

View check run for this annotation

Codecov / codecov/patch

basculehttp/challenge.go#L76

Added line #L76 was not covered by tests
}

o.WriteString(` realm="`)
Expand All @@ -100,7 +92,7 @@
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 @@
if len(bc.Scheme) > 0 {
o.WriteString(string(bc.Scheme))
} else {
o.WriteString(string(BasicScheme))
o.WriteString(string(SchemeBearer))

Check warning on line 109 in basculehttp/challenge.go

View check run for this annotation

Codecov / codecov/patch

basculehttp/challenge.go#L109

Added line #L109 was not covered by tests
}

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