loginsrv

Unnamed repository; edit this file 'description' to name the repository.
git clone git@jamesshield.xyz:repos/loginsrv.git
Log | Files | Refs | README | LICENSE

commit 5e951624dd8ea697a86f501883e3001c1e3b35b2
parent 9b6671c7bdf688b7d4874f816d2a2ecda9e8760d
Author: Shannon Wynter <me@example.com>
Date:   Thu,  8 Jun 2017 21:38:53 +1000

Add httpupstream backend

Diffstat:
MREADME.md | 16++++++++++++++++
Mcaddy/setup.go | 10++++++----
Ahttpupstream/auth.go | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahttpupstream/auth_test.go | 22++++++++++++++++++++++
Ahttpupstream/backend.go | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahttpupstream/backend_test.go | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmain.go | 4+++-
7 files changed, 290 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md @@ -26,6 +26,7 @@ The following providers (login backends) are supported. * [Htpasswd](#htpasswd) * [OSIAM](#osiam) * [Simple](#simple) (user/password pairs by configuration) +* [Httpupstreem](#httpupstream) * [Oauth2](#oauth2) * Github Login * .. Google and Facebook will come soon .. @@ -197,6 +198,21 @@ Example: loginsrv -backend 'provider=htpasswd,file=users ``` +### Httpupstreem +Authentication against an upstream http server by performing a simple simple authenticated request + +Parameters for the provider: +| Parameter-Name | Description | +| ------------------|--------------------------------------------------------| +| upstream | http/https url to call | +| skipverify | true to ignore TLS errors (optional, false by default) | +| timeout | request timeout (optional 1m by default) | + +Example: +``` +loginsrv -backend 'provider=httpupstream,upstream=https://google.com,timeout=1s' +``` + ### OSIAM [OSIAM](http://osiam.org/) is a secure identity management solution providing REST based services for authentication and authorization. It implements the multiple OAuth2 flows, as well as SCIM for managing the user data. diff --git a/caddy/setup.go b/caddy/setup.go @@ -3,17 +3,19 @@ package caddy import ( "flag" "fmt" - "github.com/mholt/caddy" - "github.com/mholt/caddy/caddyhttp/httpserver" - "github.com/tarent/loginsrv/logging" - "github.com/tarent/loginsrv/login" "os" "path" "path/filepath" "strings" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" + "github.com/tarent/loginsrv/logging" + "github.com/tarent/loginsrv/login" + // Import all backends, packaged with the caddy plugin _ "github.com/tarent/loginsrv/htpasswd" + _ "github.com/tarent/loginsrv/httpupstream" _ "github.com/tarent/loginsrv/oauth2" _ "github.com/tarent/loginsrv/osiam" ) diff --git a/httpupstream/auth.go b/httpupstream/auth.go @@ -0,0 +1,57 @@ +package httpupstream + +import ( + "crypto/tls" + "net/http" + "net/url" + "time" +) + +// Auth is the httpupstream authenticater +type Auth struct { + upstream *url.URL + skipverify bool + timeout time.Duration +} + +// NewAuth creates an httpupstream authenticater +func NewAuth(upstream *url.URL, timeout time.Duration, skipverify bool) (*Auth, error) { + a := &Auth{ + upstream: upstream, + skipverify: skipverify, + timeout: timeout, + } + + return a, nil +} + +// Authenticate the user +func (a *Auth) Authenticate(username, password string) (bool, error) { + c := &http.Client{ + Timeout: a.timeout, + } + + if a.upstream.Scheme == "https" && a.skipverify { + c.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + + req, err := http.NewRequest("GET", a.upstream.String(), nil) + if err != nil { + return false, err + } + + req.SetBasicAuth(username, password) + + resp, err := c.Do(req) + if err != nil { + return false, err + } + + if resp.StatusCode != 200 { + return false, nil + } + + return true, nil +} diff --git a/httpupstream/auth_test.go b/httpupstream/auth_test.go @@ -0,0 +1,22 @@ +package httpupstream + +import ( + "net/url" + "testing" + "time" + + . "github.com/stretchr/testify/assert" +) + +func TestAuth_UnknownUser(t *testing.T) { + ts := newTestServer() + defer ts.Close() + u, _ := url.Parse(ts.URL) + + auth, err := NewAuth(u, time.Second, false) + NoError(t, err) + + authenticated, err := auth.Authenticate("unknown", "secret") + NoError(t, err) + False(t, authenticated) +} diff --git a/httpupstream/backend.go b/httpupstream/backend.go @@ -0,0 +1,81 @@ +package httpupstream + +import ( + "errors" + "fmt" + "net/url" + "strconv" + "time" + + "github.com/tarent/loginsrv/login" + "github.com/tarent/loginsrv/model" +) + +// ProviderName const +const ProviderName = "httpupstream" + +func init() { + login.RegisterProvider( + &login.ProviderDescription{ + Name: ProviderName, + HelpText: "Httpupstream login backend opts: upstream=...,skipverify=...,timeout=...", + }, + BackendFactory) +} + +// BackendFactory creates a httpupstream backend +func BackendFactory(config map[string]string) (login.Backend, error) { + us, ue := config["upstream"] + ts, te := config["timeout"] + vs, ve := config["skipverify"] + + if !ue { + return nil, errors.New(`missing parameter "upstream" for httpupstream provider`) + } + + u, err := url.Parse(us) + if err != nil { + return nil, fmt.Errorf(`invalid parameter value "%s" in "upstream" httpupstream provider: %v`, us, err) + } + + v := false + t := time.Minute + + if te { + t, err = time.ParseDuration(ts) + if err != nil { + return nil, fmt.Errorf(`invalid parameter value "%s" in "timeout" httpupstream provider: %v`, ts, err) + } + } + + if ve { + v, err = strconv.ParseBool(vs) + if err != nil { + return nil, fmt.Errorf(`invalid parameter value "%s" in "skipverify" httpupstream provider: %v`, ts, err) + } + } + + return NewBackend(u, t, v) +} + +// Backend is a httpupstream based authentication backend. +type Backend struct { + auth *Auth +} + +// NewBackend creates a new Backend and verifies the parameters. +func NewBackend(upstream *url.URL, timeout time.Duration, skipverify bool) (*Backend, error) { + auth, err := NewAuth(upstream, timeout, skipverify) + return &Backend{ + auth, + }, err +} + +// Authenticate the user +func (sb *Backend) Authenticate(username, password string) (bool, model.UserInfo, error) { + authenticated, err := sb.auth.Authenticate(username, password) + if authenticated && err == nil { + return authenticated, model.UserInfo{Sub: username}, err + } + return false, model.UserInfo{}, err +} diff --git a/httpupstream/backend_test.go b/httpupstream/backend_test.go @@ -0,0 +1,105 @@ +package httpupstream + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + . "github.com/stretchr/testify/assert" + "github.com/tarent/loginsrv/login" +) + +func TestSetup(t *testing.T) { + p, exist := login.GetProvider(ProviderName) + True(t, exist) + NotNil(t, p) + + backend, err := p(map[string]string{ + "upstream": "https://google.com", + "skipverify": "true", + "timeout": "20s", + }) + + NoError(t, err) + Equal(t, + "https://google.com", + backend.(*Backend).auth.upstream.String()) + Equal(t, + true, + backend.(*Backend).auth.skipverify) + Equal(t, + time.Second*20, + backend.(*Backend).auth.timeout) +} + +func TestSetup_Default(t *testing.T) { + p, exist := login.GetProvider(ProviderName) + True(t, exist) + NotNil(t, p) + + backend, err := p(map[string]string{ + "upstream": "https://google.com", + }) + + NoError(t, err) + Equal(t, + "https://google.com", + backend.(*Backend).auth.upstream.String()) + Equal(t, + false, + backend.(*Backend).auth.skipverify) + Equal(t, + time.Second*60, + backend.(*Backend).auth.timeout) +} + +func TestSetup_Error(t *testing.T) { + p, exist := login.GetProvider(ProviderName) + True(t, exist) + NotNil(t, p) + + _, err := p(map[string]string{}) + Error(t, err) +} + +func TestSimpleBackend_Authenticate(t *testing.T) { + ts := newTestServer() + defer ts.Close() + u, _ := url.Parse(ts.URL) + + backend, err := NewBackend(u, time.Second, false) + NoError(t, err) + + authenticated, userInfo, err := backend.Authenticate("bob-bcrypt", "secret") + True(t, authenticated) + Equal(t, "bob-bcrypt", userInfo.Sub) + NoError(t, err) + + authenticated, userInfo, err = backend.Authenticate("bob-bcrypt", "fooo") + False(t, authenticated) + Equal(t, "", userInfo.Sub) + NoError(t, err) + + authenticated, userInfo, err = backend.Authenticate("", "") + False(t, authenticated) + Equal(t, "", userInfo.Sub) + NoError(t, err) +} + +func newTestServer() *httptest.Server { + passwordCheck := func(w http.ResponseWriter, r *http.Request) { + u, p, k := r.BasicAuth() + if !k { + w.Header().Set("WWW-Authenticate", `Basic realm="test"`) + } + + if !(u == "bob-bcrypt" && p == "secret") { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + } + + return httptest.NewServer(http.HandlerFunc(passwordCheck)) +} diff --git a/main.go b/main.go @@ -2,17 +2,19 @@ package main import ( _ "github.com/tarent/loginsrv/htpasswd" + _ "github.com/tarent/loginsrv/httpupstream" _ "github.com/tarent/loginsrv/osiam" "github.com/tarent/loginsrv/login" "context" "fmt" - "github.com/tarent/loginsrv/logging" "net/http" "os" "os/signal" "syscall" + + "github.com/tarent/loginsrv/logging" ) const applicationName = "loginsrv"