Merge pull request #547 from containous/add-basic-authentication
Add basic/digest auth
This commit is contained in:
commit
a016741918
12 changed files with 299 additions and 14 deletions
|
@ -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
|
||||
|
|
18
docs/toml.md
18
docs/toml.md
|
@ -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]
|
||||
|
|
|
@ -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
20
glide.lock
generated
|
@ -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:
|
||||
|
|
|
@ -64,8 +64,6 @@ import:
|
|||
version: b2fad6198110326662e9e356a97199078a4a775c
|
||||
subpackages:
|
||||
- acme
|
||||
- package: github.com/miekg/dns
|
||||
version: 5d001d020961ae1c184f9f8152fdc73810481677
|
||||
- package: golang.org/x/net
|
||||
subpackages:
|
||||
- context
|
||||
|
|
99
middlewares/authenticator.go
Normal file
99
middlewares/authenticator.go
Normal 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)
|
||||
}
|
103
middlewares/authenticator_test.go
Normal file
103
middlewares/authenticator_test.go
Normal 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")
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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..."
|
||||
|
|
10
server.go
10
server.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue