Skip to content

Commit

Permalink
moved to a proper v2 release
Browse files Browse the repository at this point in the history
  • Loading branch information
johnabass committed Nov 30, 2023
1 parent 93c6231 commit e62738e
Show file tree
Hide file tree
Showing 15 changed files with 815 additions and 0 deletions.
25 changes: 25 additions & 0 deletions v2/authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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
}
8 changes: 8 additions & 0 deletions v2/authorizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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
}
54 changes: 54 additions & 0 deletions v2/basculehttp/accessor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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
}
38 changes: 38 additions & 0 deletions v2/basculehttp/basicToken.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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
}
116 changes: 116 additions & 0 deletions v2/basculehttp/challenge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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)
}
}
137 changes: 137 additions & 0 deletions v2/basculehttp/frontDoor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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)
})
}
Loading

0 comments on commit e62738e

Please sign in to comment.