Add httpupstream backend

This commit is contained in:
Shannon Wynter
2017-06-08 21:38:53 +10:00
parent 9b6671c7bd
commit 5e951624dd
7 changed files with 290 additions and 5 deletions
+16
View File
@@ -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.
+6 -4
View File
@@ -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"
)
+57
View File
@@ -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
}
+22
View File
@@ -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)
}
+81
View File
@@ -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
}
+105
View File
@@ -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))
}
+3 -1
View File
@@ -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"