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

refactored challenge API #281

Merged
merged 1 commit into from
Aug 16, 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
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
Loading