Skip to content

Commit

Permalink
refactored challenge API
Browse files Browse the repository at this point in the history
  • Loading branch information
johnabass committed Aug 16, 2024
1 parent b82c8f1 commit 04d0232
Show file tree
Hide file tree
Showing 5 changed files with 477 additions and 90 deletions.
7 changes: 0 additions & 7 deletions basculehttp/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,6 @@ var (
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.
//
Expand Down
260 changes: 188 additions & 72 deletions basculehttp/challenge.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,115 +4,231 @@
package basculehttp

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

const (
// WwwAuthenticateHeaderName is the HTTP header used for StatusUnauthorized challenges.
WwwAuthenticateHeaderName = "WWW-Authenticate"
// WWWAuthenticateHeaderName is the HTTP header used for StatusUnauthorized challenges
// when encountered by the Middleware.
//
// This value is used by default when no header is supplied to Challenges.WriteHeader.
WWWAuthenticateHeaderName = "WWW-Authenticate"
)

var (
// ErrInvalidChallengeScheme indicates that a scheme was improperly formatted. Usually,
// this methods the scheme was either blank or contained whitespace.
ErrInvalidChallengeScheme = errors.New("Invalid challenge auth scheme")

// DefaultBasicRealm is the realm used for a basic challenge
// when no realm is supplied.
DefaultBasicRealm string = "bascule"
// ErrInvalidChallengeParameter indicates that an attempt was made to an a challenge
// auth parameter that wasn't validly formatted. Usually, this means that the
// name contained whitespace.
ErrInvalidChallengeParameter = errors.New("Invalid challenge auth parameter")

// DefaultBearerRealm is the realm used for a bearer challenge
// when no realm is supplied.
DefaultBearerRealm string = "bascule"
// ErrReservedChallengeParameter indicates that an attempt was made to add a
// challenge auth parameter that was reserved by the RFC.
ErrReservedChallengeParameter = errors.New("Reserved challenge auth parameter")
)

// Challenge represents a WWW-Authenticate challenge.
type Challenge interface {
// FormatAuthenticate formats the authenticate string.
FormatAuthenticate(strings.Builder)
// reservedChallengeParameterNames holds the names of reserved challenge auth parameters
// that cannot be added to a ChallengeParameters.
var reservedChallengeParameterNames = map[string]bool{
"realm": true,
"token68": true,
}

// Challenges represents a sequence of challenges to associated with
// a StatusUnauthorized response.
type Challenges []Challenge
// ChallengeParameters holds the set of parameters. The zero value of this
// type is ready to use. This type handles writing parameters as well as
// provides commonly used parameter names for convenience.
type ChallengeParameters struct {
names, values []string
byName map[string]int // the parameter index
}

// Add appends challenges to this set.
func (chs *Challenges) Add(ch ...Challenge) {
if *chs == nil {
*chs = make(Challenges, 0, len(ch))
// Len returns the number of name/value pairs contained in these parameters.
func (cp *ChallengeParameters) Len() int {
return len(cp.names)
}

// Set sets the value of a parameter. If a parameter was already set, it is
// ovewritten.
//
// If the parameter name is invalid, this method raises an error.
func (cp *ChallengeParameters) Set(name, value string) (err error) {
switch {
case len(name) == 0:
err = ErrInvalidChallengeParameter

case fastContainsSpace(name):
err = ErrInvalidChallengeParameter

case reservedChallengeParameterNames[name]:
err = ErrReservedChallengeParameter

default:
if i, exists := cp.byName[name]; exists {
cp.values[i] = value
} else {
if cp.byName == nil {
cp.byName = make(map[string]int)
}

cp.byName[name] = len(cp.names)
cp.names = append(cp.names, name)
cp.values = append(cp.values, value)
}
}

*chs = append(*chs, ch...)
return
}

// WriteHeader inserts one WWW-Authenticate header per challenge in this set.
// If this set is empty, the given http.Header is not modified.
func (chs Challenges) WriteHeader(h http.Header) {
// Charset sets a charset auth parameter. Basic auth is the main scheme
// that uses this.
func (cp *ChallengeParameters) Charset(value string) error {
return cp.Set("charset", value)
}

// Write formats this challenge to the given builder.
func (cp *ChallengeParameters) Write(o *strings.Builder) {
for i := 0; i < len(cp.names); i++ {
if i > 0 {
o.WriteString(", ")
}

o.WriteString(cp.names[i])
o.WriteString(`="`)
o.WriteString(cp.values[i])
o.WriteRune('"')
}
}

// String returns the RFC 7235 format of these parameters.
func (cp *ChallengeParameters) String() string {
var o strings.Builder
for _, ch := range chs {
ch.FormatAuthenticate(o)
h.Add(WwwAuthenticateHeaderName, o.String())
o.Reset()
cp.Write(&o)
return o.String()
}

// NewChallengeParameters creates a ChallengeParameters from a sequence of name/value pairs.
// The strings are expected to be in name, value, name, value, ... sequence. If the number
// of strings is odd, then the last parameter will have a blank value.
//
// If any error occurs while setting parameters, execution is halted and that
// error is returned.
func NewChallengeParameters(s ...string) (cp ChallengeParameters, err error) {
for i, j := 0, 1; err == nil && i < len(s); i, j = i+2, j+2 {
if j < len(s) {
err = cp.Set(s[i], s[j])
} else {
err = cp.Set(s[i], "")
}
}

return
}

// 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.
// Challenge represets an HTTP authentication challenge, as defined by RFC 7235.
type Challenge struct {
// Scheme is the name of scheme supplied in the challenge. This field is required.
Scheme 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 is the name of the realm for the challenge. This field is
// optional, but it is HIGHLY recommended to set it to something useful
// to a client.
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
// Token68 controls whether the token68 flag is written in the challenge.
Token68 bool

// Parameters are the optional auth parameters.
Parameters ChallengeParameters
}

func (bc BasicChallenge) FormatAuthenticate(o strings.Builder) {
if len(bc.Scheme) > 0 {
o.WriteString(string(bc.Scheme))
} else {
o.WriteString(string(SchemeBasic))
// Write formats this challenge to the given builder. Any error halts
// formatting and that error is returned.
func (c Challenge) Write(o *strings.Builder) (err error) {
s := string(c.Scheme)
switch {
case len(s) == 0:
err = ErrInvalidChallengeScheme

case fastContainsSpace(s):
err = ErrInvalidChallengeScheme

default:
o.WriteString(s)
if len(c.Realm) > 0 {
o.WriteString(` realm="`)
o.WriteString(c.Realm)
o.WriteRune('"')
}

if c.Token68 {
o.WriteString(" token68")
}

if c.Parameters.Len() > 0 {
o.WriteRune(' ')
c.Parameters.Write(o)
}
}

o.WriteString(` realm="`)
if len(bc.Realm) > 0 {
o.WriteString(bc.Realm)
} else {
o.WriteString(DefaultBasicRealm)
return
}

// NewBasicChallenge is a convenience for creating a Challenge for basic auth.
//
// Although realm is optional, it is HIGHLY recommended to set it to something
// recognizable for a client.
func NewBasicChallenge(realm string, UTF8 bool) (c Challenge, err error) {
c = Challenge{
Scheme: SchemeBasic,
Realm: realm,
}

o.WriteRune('"')
if bc.UTF8 {
o.WriteString(`, charset="UTF-8"`)
if UTF8 {
err = c.Parameters.Charset("UTF-8")
}

return
}

type BearerChallenge struct {
// Scheme is the name of scheme supplied in the challenge. If this
// field is unset, BearerScheme is used.
Scheme Scheme
// Challenges represents a sequence of challenges to associated with
// a StatusUnauthorized response.
type Challenges []Challenge

// 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
// Append appends challenges to this set. The semantics of this
// method are the same as the built-in append.
func (chs Challenges) Append(ch ...Challenge) Challenges {
return append(chs, ch...)
}

func (bc BearerChallenge) FormatAuthenticate(o strings.Builder) {
if len(bc.Scheme) > 0 {
o.WriteString(string(bc.Scheme))
} else {
o.WriteString(string(SchemeBearer))
// WriteHeader inserts one Http authenticate header per challenge in this set.
// If this set is empty, the given http.Header is not modified.
//
// The name is used as the header name for each header this method writes.
// Typically, this will be WWW-Authenticate or Proxy-Authenticate. If name
// is blank, WWWAuthenticateHeaderName is used.
//
// If any challenge returns an error during formatting, execution is
// halted and that error is returned.
func (chs Challenges) WriteHeader(name string, h http.Header) error {
if len(name) == 0 {
name = WWWAuthenticateHeaderName
}

o.WriteString(` realm="`)
if len(bc.Realm) > 0 {
o.WriteString(bc.Realm)
} else {
o.WriteString(DefaultBasicRealm)
var o strings.Builder
for _, ch := range chs {
err := ch.Write(&o)
if err != nil {
return err
}

h.Add(name, o.String())
o.Reset()
}

return nil
}
Loading

0 comments on commit 04d0232

Please sign in to comment.