Merge pull request #547 from containous/add-basic-authentication

Add basic/digest auth
This commit is contained in:
Emile Vauge 2016-07-28 19:01:46 +02:00 committed by GitHub
commit a016741918
12 changed files with 299 additions and 14 deletions

View file

@ -163,6 +163,7 @@ type EntryPoint struct {
Address string
TLS *TLS
Redirect *Redirect
Auth *types.Auth
}
// Redirect configures a redirection of an entry point to another, or to an URL

View file

@ -110,7 +110,23 @@
# CertFile = "integration/fixtures/https/snitest.org.cert"
# KeyFile = "integration/fixtures/https/snitest.org.key"
#
# To enable basic auth on an entrypoint
# with 2 user/pass: test:test and test2:test2
# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones
# [entryPoints]
# [entryPoints.http]
# address = ":80"
# [entryPoints.http.auth.basic]
# users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
#
# To enable digest auth on an entrypoint
# with 2 user/realm/pass: test:traefik:test and test2:traefik:test2
# You can use htdigest to generate those ones
# [entryPoints]
# [entryPoints.http]
# address = ":80"
# [entryPoints.http.auth.basic]
# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"]
[entryPoints]
[entryPoints.http]

View file

@ -97,3 +97,21 @@ entryPoint = "https"
backend = "backend2"
rule = "Path:/test"
```
## Enable Basic authentication in an entrypoint
With two user/pass:
- `test`:`test`
- `test2`:`test2`
Passwords are encoded in MD5: you can use htpasswd to generate those ones.
```
defaultEntryPoints = ["http"]
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.http.auth.basic]
users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
```

20
glide.lock generated
View file

@ -1,5 +1,5 @@
hash: 745d05424943c3345ff5fca5b121c6af3930f62fc13195d87d9fcce6686620ea
updated: 2016-07-22T15:14:53.608798979+02:00
hash: 5cb432175705882247ac2cbf708c879fad8b287afe9e1e18f06dbce1e956acd2
updated: 2016-07-28T18:20:42.864416381+02:00
imports:
- name: github.com/abbot/go-http-auth
version: cb4372376e1e00e9f6ab9ec142e029302c9e7140
@ -36,7 +36,7 @@ imports:
subpackages:
- spew
- name: github.com/docker/distribution
version: 2b72dd3927b2958160a2336f16145c0c421aa6a4
version: 857d0f15c0a4d8037175642e0ca3660829551cb5
subpackages:
- reference
- digest
@ -141,7 +141,7 @@ imports:
- lookup
- version
- name: github.com/docker/libkv
version: aabc039ad04deb721e234f99cd1b4aa28ac71a40
version: 35d3e2084c650109e7bcc7282655b1bc8ba924ff
subpackages:
- store
- store/boltdb
@ -157,7 +157,7 @@ imports:
- name: github.com/go-check/check
version: 4f90aeace3a26ad7021961c297b22c42160c7b25
- name: github.com/gogo/protobuf
version: 6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b
version: e57a569e1882958f6b188cb42231d6db87701f2a
subpackages:
- proto
- name: github.com/golang/glog
@ -169,7 +169,7 @@ imports:
- name: github.com/gorilla/context
version: aed02d124ae4a0e94fea4541c8effd05bf0c8296
- name: github.com/hashicorp/consul
version: fce7d75609a04eeb9d4bf41c8dc592aac18fc97d
version: 8a8271fd81cdaa1bbc20e4ced86531b90c7eaf79
subpackages:
- api
- name: github.com/hashicorp/go-cleanhttp
@ -220,13 +220,13 @@ imports:
- name: github.com/miekg/dns
version: 5d001d020961ae1c184f9f8152fdc73810481677
- name: github.com/mitchellh/mapstructure
version: 21a35fb16463dfb7c8eee579c65d995d95e64d1e
version: d2dd0262208475919e1a362f675cfc0e7c10e905
- name: github.com/moul/http2curl
version: b1479103caacaa39319f75e7f57fc545287fca0d
- name: github.com/ogier/pflag
version: 45c278ab3607870051a2ea9040bb85fcb8557481
- name: github.com/opencontainers/runc
version: fb221651e5120cd287a76c7c1b6c877520fbd034
version: bd1d3ac0480c5d3babac10dc32cff2886563219c
subpackages:
- libcontainer/user
- name: github.com/parnurzeal/gorequest
@ -294,9 +294,11 @@ imports:
subpackages:
- acme
- name: golang.org/x/crypto
version: 911fafb28f4ee7c7bd483539a6c96190bbbccc3f
version: d81fdb778bf2c40a91b24519d60cdc5767318829
subpackages:
- ocsp
- bcrypt
- blowfish
- name: golang.org/x/net
version: b400c2eff1badec7022a8c8f5bea058b6315eed7
subpackages:

View file

@ -64,8 +64,6 @@ import:
version: b2fad6198110326662e9e356a97199078a4a775c
subpackages:
- acme
- package: github.com/miekg/dns
version: 5d001d020961ae1c184f9f8152fdc73810481677
- package: golang.org/x/net
subpackages:
- context

View file

@ -0,0 +1,99 @@
package middlewares
import (
"fmt"
log "github.com/Sirupsen/logrus"
"github.com/abbot/go-http-auth"
"github.com/codegangsta/negroni"
"github.com/containous/traefik/types"
"net/http"
"strings"
)
// Authenticator is a middleware that provides HTTP basic and digest authentication
type Authenticator struct {
handler negroni.Handler
users map[string]string
}
// NewAuthenticator builds a new Autenticator given a config
func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) {
if authConfig == nil {
return nil, fmt.Errorf("Error creating Authenticator: auth is nil")
}
var err error
authenticator := Authenticator{}
if authConfig.Basic != nil {
authenticator.users, err = parserBasicUsers(authConfig.Basic.Users)
if err != nil {
return nil, err
}
basicAuth := auth.NewBasicAuthenticator("traefik", authenticator.secretBasic)
authenticator.handler = negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if username := basicAuth.CheckAuth(r); username == "" {
log.Debugf("Auth failed...")
basicAuth.RequireAuth(w, r)
} else {
next.ServeHTTP(w, r)
}
})
} else if authConfig.Digest != nil {
authenticator.users, err = parserDigestUsers(authConfig.Digest.Users)
if err != nil {
return nil, err
}
digestAuth := auth.NewDigestAuthenticator("traefik", authenticator.secretDigest)
authenticator.handler = negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if username, _ := digestAuth.CheckAuth(r); username == "" {
digestAuth.RequireAuth(w, r)
} else {
next.ServeHTTP(w, r)
}
})
}
return &authenticator, nil
}
func parserBasicUsers(users types.Users) (map[string]string, error) {
userMap := make(map[string]string)
for _, user := range users {
split := strings.Split(user, ":")
if len(split) != 2 {
return nil, fmt.Errorf("Error parsing Authenticator user: %v", user)
}
userMap[split[0]] = split[1]
}
return userMap, nil
}
func parserDigestUsers(users types.Users) (map[string]string, error) {
userMap := make(map[string]string)
for _, user := range users {
split := strings.Split(user, ":")
if len(split) != 3 {
return nil, fmt.Errorf("Error parsing Authenticator user: %v", user)
}
userMap[split[0]+":"+split[1]] = split[2]
}
return userMap, nil
}
func (a *Authenticator) secretBasic(user, realm string) string {
if secret, ok := a.users[user]; ok {
return secret
}
log.Debugf("User not found: %s", user)
return ""
}
func (a *Authenticator) secretDigest(user, realm string) string {
if secret, ok := a.users[user+":"+realm]; ok {
return secret
}
log.Debugf("User not found: %s:%s", user, realm)
return ""
}
func (a *Authenticator) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
a.handler.ServeHTTP(rw, r, next)
}

View file

@ -0,0 +1,103 @@
package middlewares
import (
"fmt"
"github.com/codegangsta/negroni"
"github.com/containous/traefik/types"
"github.com/stretchr/testify/assert"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
func TestBasicAuthFail(t *testing.T) {
authMiddleware, err := NewAuthenticator(&types.Auth{
Basic: &types.Basic{
Users: []string{"test"},
},
})
assert.Contains(t, err.Error(), "Error parsing Authenticator user", "should contains")
authMiddleware, err = NewAuthenticator(&types.Auth{
Basic: &types.Basic{
Users: []string{"test:test"},
},
})
assert.NoError(t, err, "there should be no error")
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")
})
n := negroni.New(authMiddleware)
n.UseHandler(handler)
ts := httptest.NewServer(n)
defer ts.Close()
client := &http.Client{}
req, err := http.NewRequest("GET", ts.URL, nil)
req.SetBasicAuth("test", "test")
res, err := client.Do(req)
assert.NoError(t, err, "there should be no error")
assert.Equal(t, http.StatusUnauthorized, res.StatusCode, "they should be equal")
}
func TestBasicAuthSuccess(t *testing.T) {
authMiddleware, err := NewAuthenticator(&types.Auth{
Basic: &types.Basic{
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"},
},
})
assert.NoError(t, err, "there should be no error")
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")
})
n := negroni.New(authMiddleware)
n.UseHandler(handler)
ts := httptest.NewServer(n)
defer ts.Close()
client := &http.Client{}
req, err := http.NewRequest("GET", ts.URL, nil)
req.SetBasicAuth("test", "test")
res, err := client.Do(req)
assert.NoError(t, err, "there should be no error")
assert.Equal(t, http.StatusOK, res.StatusCode, "they should be equal")
body, err := ioutil.ReadAll(res.Body)
assert.NoError(t, err, "there should be no error")
assert.Equal(t, "traefik\n", string(body), "they should be equal")
}
func TestDigestAuthFail(t *testing.T) {
authMiddleware, err := NewAuthenticator(&types.Auth{
Digest: &types.Digest{
Users: []string{"test"},
},
})
assert.Contains(t, err.Error(), "Error parsing Authenticator user", "should contains")
authMiddleware, err = NewAuthenticator(&types.Auth{
Digest: &types.Digest{
Users: []string{"test:traefik:test"},
},
})
assert.NoError(t, err, "there should be no error")
assert.NotNil(t, authMiddleware, "this should not be nil")
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")
})
n := negroni.New(authMiddleware)
n.UseHandler(handler)
ts := httptest.NewServer(n)
defer ts.Close()
client := &http.Client{}
req, err := http.NewRequest("GET", ts.URL, nil)
req.SetBasicAuth("test", "test")
res, err := client.Do(req)
assert.NoError(t, err, "there should be no error")
assert.Equal(t, http.StatusUnauthorized, res.StatusCode, "they should be equal")
}

View file

@ -21,5 +21,7 @@ echo "Updating docker containous/traefik image..."
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
docker tag containous/traefik containous/traefik:pr-${PR}
docker push containous/traefik:pr-${PR}
docker tag containous/traefik containous/traefik:experimental
docker push containous/traefik:experimental
echo "Deployed"

View file

@ -27,7 +27,7 @@ sudo chmod +x /usr/bin/ghr
# github release and tag
echo "Github release..."
ghr -t $GITHUB_TOKEN -u containous -r traefik --prerelease ${VERSION} dist/
ghr -t $GITHUB_TOKEN -u containous -r traefik ${VERSION} dist/
# update docs.traefik.io
echo "Generating and updating documentation..."

View file

@ -118,7 +118,15 @@ func (server *Server) Close() {
func (server *Server) startHTTPServers() {
server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration)
for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints {
newsrv, err := server.prepareServer(newServerEntryPointName, newServerEntryPoint.httpRouter, server.globalConfiguration.EntryPoints[newServerEntryPointName], nil, server.loggerMiddleware, metrics)
serverMiddlewares := []negroni.Handler{server.loggerMiddleware, metrics}
if server.globalConfiguration.EntryPoints[newServerEntryPointName].Auth != nil {
authMiddleware, err := middlewares.NewAuthenticator(server.globalConfiguration.EntryPoints[newServerEntryPointName].Auth)
if err != nil {
log.Fatal("Error starting server: ", err)
}
serverMiddlewares = append(serverMiddlewares, authMiddleware)
}
newsrv, err := server.prepareServer(newServerEntryPointName, newServerEntryPoint.httpRouter, server.globalConfiguration.EntryPoints[newServerEntryPointName], nil, serverMiddlewares...)
if err != nil {
log.Fatal("Error preparing server: ", err)
}

View file

@ -143,6 +143,25 @@
# [entryPoints.http.redirect]
# regex = "^http://localhost/(.*)"
# replacement = "http://mydomain/$1"
#
# To enable basic auth on an entrypoint
# with 2 user/pass: test:test and test2:test2
# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones
# [entryPoints]
# [entryPoints.http]
# address = ":80"
# [entryPoints.http.auth.basic]
# users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
#
# To enable digest auth on an entrypoint
# with 2 user/realm/pass: test:traefik:test and test2:traefik:test2
# You can use htdigest to generate those ones
# [entryPoints]
# [entryPoints.http]
# address = ":80"
# [entryPoints.http.auth.basic]
# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"]
# Enable retry sending request if network error
#

View file

@ -183,3 +183,22 @@ func (cs *Constraints) SetValue(val interface{}) {
func (cs *Constraints) Type() string {
return fmt.Sprint("constraint")
}
// Auth holds authentication configuration (BASIC, DIGEST, users)
type Auth struct {
Basic *Basic
Digest *Digest
}
// Users authentication users
type Users []string
// Basic HTTP basic authentication
type Basic struct {
Users
}
// Digest HTTP authentication
type Digest struct {
Users
}