Skip to content

Commit

Permalink
Context support for Authenticators.
Browse files Browse the repository at this point in the history
  • Loading branch information
abbot committed Sep 22, 2015
1 parent efc9484 commit ee33244
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 1 deletion.
57 changes: 56 additions & 1 deletion auth.go
@@ -1,7 +1,11 @@
// Package auth is an implementation of HTTP Basic and HTTP Digest authentication.
package auth

import "net/http"
import (
"net/http"

"golang.org/x/net/context"
)

/*
Request handlers must take AuthenticatedRequest instead of http.Request
Expand Down Expand Up @@ -37,10 +41,61 @@ type AuthenticatedHandlerFunc func(http.ResponseWriter, *AuthenticatedRequest)
*/
type Authenticator func(AuthenticatedHandlerFunc) http.HandlerFunc

// Info contains authentication information for the request.
type Info struct {
// Authenticated is set to true when request was authenticated
// successfully, i.e. username and password passed in request did
// pass the check.
Authenticated bool

// Username contains a user name passed in the request when
// Authenticated is true. It's value is undefined if Authenticated
// is false.
Username string

// ResponseHeaders contains extra headers that must be set by server
// when sending back HTTP response.
ResponseHeaders http.Header
}

// UpdateHeaders updates headers with this Info's ResponseHeaders. It is
// safe to call this function on nil Info.
func (i *Info) UpdateHeaders(headers http.Header) {
if i == nil {
return
}
for k, values := range i.ResponseHeaders {
for _, v := range values {
headers.Add(k, v)
}
}
}

type key int // used for context keys

var infoKey key = 0

type AuthenticatorInterface interface {
// NewContext returns a new context carrying authentication
// information extracted from the request.
NewContext(ctx context.Context, r *http.Request) context.Context

// Wrap returns an http.HandlerFunc which wraps
// AuthenticatedHandlerFunc with this authenticator's
// authentication checks.
Wrap(AuthenticatedHandlerFunc) http.HandlerFunc
}

// FromContext returns authentication information from the context or
// nil if no such information present.
func FromContext(ctx context.Context) *Info {
info, ok := ctx.Value(infoKey).(*Info)
if !ok {
return nil
}
return info
}

func JustCheck(auth AuthenticatorInterface, wrapped http.HandlerFunc) http.HandlerFunc {
return auth.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) {
ar.Header.Set("X-Authenticated-Username", ar.Username)
Expand Down
14 changes: 14 additions & 0 deletions basic.go
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"golang.org/x/crypto/bcrypt"
"golang.org/x/net/context"
)

type compareFunc func(hashedPassword, password []byte) error
Expand All @@ -32,6 +33,9 @@ type BasicAuth struct {
Secrets SecretProvider
}

// check that BasicAuth implements AuthenticatorInterface
var _ = (AuthenticatorInterface)((*BasicAuth)(nil))

/*
Checks the username/password combination from the request. Returns
either an empty string (authentication failed) or the name of the
Expand Down Expand Up @@ -121,6 +125,16 @@ func (a *BasicAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc {
}
}

// NewContext returns a context carrying authentication information for the request.
func (a *BasicAuth) NewContext(ctx context.Context, r *http.Request) context.Context {
info := &Info{Username: a.CheckAuth(r), ResponseHeaders: make(http.Header)}
info.Authenticated = (info.Username != "")
if !info.Authenticated {
info.ResponseHeaders.Set("WWW-Authenticate", `Basic realm="`+a.Realm+`"`)
}
return context.WithValue(ctx, infoKey, info)
}

func NewBasicAuthenticator(realm string, secrets SecretProvider) *BasicAuth {
return &BasicAuth{Realm: realm, Secrets: secrets}
}
26 changes: 26 additions & 0 deletions digest.go
Expand Up @@ -10,6 +10,8 @@ import (
"strings"
"sync"
"time"

"golang.org/x/net/context"
)

type digest_client struct {
Expand All @@ -36,6 +38,9 @@ type DigestAuth struct {
mutex sync.Mutex
}

// check that DigestAuth implements AuthenticatorInterface
var _ = (AuthenticatorInterface)((*DigestAuth)(nil))

type digest_cache_entry struct {
nonce string
last_seen int64
Expand Down Expand Up @@ -214,6 +219,27 @@ func (a *DigestAuth) JustCheck(wrapped http.HandlerFunc) http.HandlerFunc {
})
}

// NewContext returns a context carrying authentication information for the request.
func (a *DigestAuth) NewContext(ctx context.Context, r *http.Request) context.Context {
username, authinfo := a.CheckAuth(r)
info := &Info{Username: username, ResponseHeaders: make(http.Header)}
if username != "" {
info.Authenticated = true
info.ResponseHeaders.Set("Authentication-Info", *authinfo)
} else {
// return back digest WWW-Authenticate header
if len(a.clients) > a.ClientCacheSize+a.ClientCacheTolerance {
a.Purge(a.ClientCacheTolerance * 2)
}
nonce := RandomKey()
a.clients[nonce] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()}
info.ResponseHeaders.Set("WWW-Authenticate",
fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm="MD5", qop="auth"`,
a.Realm, nonce, a.Opaque))
}
return context.WithValue(ctx, infoKey, info)
}

func NewDigestAuthenticator(realm string, secrets SecretProvider) *DigestAuth {
da := &DigestAuth{
Opaque: RandomKey(),
Expand Down
60 changes: 60 additions & 0 deletions examples/context.go
@@ -0,0 +1,60 @@
// +build ignore

/*
Example application using NewContext/FromContext
Build with:
go build context.go
*/

package main

import (
"fmt"
"net/http"

auth ".."
"golang.org/x/net/context"
)

func Secret(user, realm string) string {
if user == "john" {
// password is "hello"
return "b98e16cbc3d01734b264adba7baa3bf9"
}
return ""
}

type ContextHandler interface {
ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request)
}

type ContextHandlerFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request)

func (f ContextHandlerFunc) ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) {
f(ctx, w, r)
}

func handle(ctx context.Context, w http.ResponseWriter, r *http.Request) {
authInfo := auth.FromContext(ctx)
authInfo.UpdateHeaders(w.Header())
if authInfo == nil || !authInfo.Authenticated {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "<html><body><h1>Hello, %s!</h1></body></html>", authInfo.Username)
}

func authenticatedHandler(a auth.AuthenticatorInterface, h ContextHandler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := a.NewContext(context.Background(), r)
h.ServeHTTP(ctx, w, r)
})
}

func main() {
authenticator := auth.NewDigestAuthenticator("example.com", Secret)
http.Handle("/", authenticatedHandler(authenticator, ContextHandlerFunc(handle)))
http.ListenAndServe(":8080", nil)
}

0 comments on commit ee33244

Please sign in to comment.