Allow for dynamic redirects using query parameters and cookies. Allow redirects to external domains by whitelist only. Check referer header before allowing a redirect. For https://github.com/tarent/loginsrv/issues/45

This commit is contained in:
luke.hopkins
2017-11-23 16:24:55 +00:00
parent dbba63f98e
commit 3f9fb92386
8 changed files with 419 additions and 181 deletions
+28 -23
View File
@@ -42,29 +42,34 @@ For questions and support please use the [Gitter chat room](https://gitter.im/ta
_Note for Caddy users_: Not all parameters are available in Caddy. See the table for details. With Caddy, the parameter names can be also be used with `_` in the names, e.g. `cookie_http_only`.
| Parameter | Type | Default | Caddy | Description |
|-------------------|-------------|--------------|-------|--------------------------------------------------------------------------------------|
| -cookie-domain | string | | X | The optional domain parameter for the cookie |
| -cookie-expiry | string | session | X | The expiry duration for the cookie, e.g. 2h or 3h30m |
| -cookie-http-only | boolean | true | X | Set the cookie with the http only flag |
| -cookie-name | string | "jwt_token" | X | The name of the jwt cookie |
| -github | value | | X | Oauth config in the form: client_id=..,client_secret=..[,scope=..,][redirect_uri=..] |
| -google | value | | X | Oauth config in the form: client_id=..,client_secret=..,scope=..[redirect_uri=..] |
| -host | string | "localhost" | - | The host to listen on |
| -htpasswd | value | | X | Htpasswd login backend opts: file=/path/to/pwdfile |
| -jwt-expiry | go duration | 24h | X | The expiry duration for the jwt token, e.g. 2h or 3h30m |
| -jwt-secret | string | "random key" | X | The secret to sign the jwt token |
| -log-level | string | "info" | - | The log level |
| -login-path | string | "/login" | X | The path of the login resource |
| -logout-url | string | | X | The url or path to redirect after logout |
| -osiam | value | | X | OSIAM login backend opts: endpoint=..,client_id=..,client_secret=.. |
| -port | string | "6789" | - | The port to listen on |
| -simple | value | | X | Simple login backend opts: user1=password,user2=password,.. |
| -success-url | string | "/" | X | The url to redirect after login |
| -template | string | | X | An alternative template for the login form |
| -text-logging | boolean | true | - | Log in text format instead of json |
| -jwt-refreshes | int | 0 | X | The maximum amount of jwt refreshes. |
| -grace-period | go duration | 5s | - | Duration to wait after SIGINT/SIGTERM for existing requests. No new requests are accepted. |
| Parameter | Type | Default | Caddy | Description |
|-----------------------------|-------------|--------------|-------|--------------------------------------------------------------------------------------------|
| -cookie-domain | string | | X | The optional domain parameter for the cookie |
| -cookie-expiry | string | session | X | The expiry duration for the cookie, e.g. 2h or 3h30m |
| -cookie-http-only | boolean | true | X | Set the cookie with the http only flag |
| -cookie-name | string | "jwt_token" | X | The name of the jwt cookie |
| -github | value | | X | Oauth config in the form: client_id=..,client_secret=..[,scope=..,][redirect_uri=..] |
| -google | value | | X | Oauth config in the form: client_id=..,client_secret=..,scope=..[redirect_uri=..] |
| -host | string | "localhost" | - | The host to listen on |
| -htpasswd | value | | X | Htpasswd login backend opts: file=/path/to/pwdfile |
| -jwt-expiry | go duration | 24h | X | The expiry duration for the jwt token, e.g. 2h or 3h30m |
| -jwt-secret | string | "random key" | X | The secret to sign the jwt token |
| -log-level | string | "info" | - | The log level |
| -login-path | string | "/login" | X | The path of the login resource |
| -logout-url | string | | X | The url or path to redirect after logout |
| -osiam | value | | X | OSIAM login backend opts: endpoint=..,client_id=..,client_secret=.. |
| -port | string | "6789" | - | The port to listen on |
| -simple | value | | X | Simple login backend opts: user1=password,user2=password,.. |
| -success-url | string | "/" | X | The url to redirect after login |
| -allow-redirects | boolean | true | X | Allow dynamic redirects |
| -redirect-query-parameter | string | "backTo" | X | The query parameter to find the dynamic redirect in |
| -prevent-external-redirects | boolean | true | X | Prevent dynamic redirects to external domains |
| -check-referer-on-redirects | boolean | true | X | Check the referer header to ensure it matches the host header on dynamic redirects |
| -whitelist-domains-file | string | "" | X | File containing whitelist of domains for dynamic redirects, one domain per line |
| -template | string | | X | An alternative template for the login form |
| -text-logging | boolean | true | - | Log in text format instead of json |
| -jwt-refreshes | int | 0 | X | The maximum amount of jwt refreshes. |
| -grace-period | go duration | 5s | - | Duration to wait after SIGINT/SIGTERM for existing requests. No new requests are accepted. |
### Environment Variables
All of the above Config Options can also be applied as environment variable, where the name is written in the way: `LOGINSRV_OPTION_NAME`.
+21
View File
@@ -50,3 +50,24 @@ login {
simple bob=secret,alice=secret
}
```
### Example caddyfile with dynamic redirects
```
127.0.0.1
root {$PWD}
browse
jwt {
path /
except /favicon.ico
redirect /login?backTo={rewrite_uri}
allow sub bob
allow sub alice
}
login {
simple bob=secret,alice=secret
check_referer_on_redirects false
}
```
+62 -38
View File
@@ -2,15 +2,16 @@ package caddy
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
. "github.com/stretchr/testify/assert"
"github.com/tarent/loginsrv/login"
"io/ioutil"
"path/filepath"
"os"
"testing"
"time"
)
func TestSetup(t *testing.T) {
@@ -28,12 +29,16 @@ func TestSetup(t *testing.T) {
}`,
shouldErr: false,
config: login.Config{
JwtSecret: "jwtsecret",
JwtExpiry: 24 * time.Hour,
SuccessURL: "/",
LoginPath: "/login",
CookieName: "jwt_token",
CookieHTTPOnly: true,
JwtSecret: "jwtsecret",
JwtExpiry: 24 * time.Hour,
SuccessURL: "/",
AllowRedirects: true,
RedirectQueryParameter: "backTo",
PreventExternalRedirects: true,
CheckRefererOnRedirects: true,
LoginPath: "/login",
CookieName: "jwt_token",
CookieHTTPOnly: true,
Backends: login.Options{
"simple": map[string]string{
"bob": "secret",
@@ -47,6 +52,9 @@ func TestSetup(t *testing.T) {
success_url successurl
jwt_expiry 42h
login_path /foo/bar
allow-redirects true
prevent-external-redirects true
check-referer-on-redirects true
cookie_name cookiename
cookie_http_only false
cookie_domain example.com
@@ -56,14 +64,18 @@ func TestSetup(t *testing.T) {
}`,
shouldErr: false,
config: login.Config{
JwtSecret: "jwtsecret",
JwtExpiry: 42 * time.Hour,
SuccessURL: "successurl",
LoginPath: "/foo/bar",
CookieName: "cookiename",
CookieDomain: "example.com",
CookieExpiry: 23*time.Hour + 23*time.Minute,
CookieHTTPOnly: false,
JwtSecret: "jwtsecret",
JwtExpiry: 42 * time.Hour,
SuccessURL: "successurl",
AllowRedirects: true,
RedirectQueryParameter: "backTo",
PreventExternalRedirects: true,
CheckRefererOnRedirects: true,
LoginPath: "/foo/bar",
CookieName: "cookiename",
CookieDomain: "example.com",
CookieExpiry: 23*time.Hour + 23*time.Minute,
CookieHTTPOnly: false,
Backends: login.Options{
"simple": map[string]string{
"bob": "secret",
@@ -87,12 +99,16 @@ func TestSetup(t *testing.T) {
}`,
shouldErr: false,
config: login.Config{
JwtSecret: "jwtsecret",
JwtExpiry: 24 * time.Hour,
SuccessURL: "/",
LoginPath: "/context/login",
CookieName: "cookiename",
CookieHTTPOnly: true,
JwtSecret: "jwtsecret",
JwtExpiry: 24 * time.Hour,
SuccessURL: "/",
AllowRedirects: true,
RedirectQueryParameter: "backTo",
PreventExternalRedirects: true,
CheckRefererOnRedirects: true,
LoginPath: "/context/login",
CookieName: "cookiename",
CookieHTTPOnly: true,
Backends: login.Options{
"simple": map[string]string{
"bob": "secret",
@@ -111,12 +127,16 @@ func TestSetup(t *testing.T) {
}`,
shouldErr: false,
config: login.Config{
JwtSecret: "jwtsecret",
JwtExpiry: 24 * time.Hour,
SuccessURL: "/",
LoginPath: "/login",
CookieName: "cookiename",
CookieHTTPOnly: true,
JwtSecret: "jwtsecret",
JwtExpiry: 24 * time.Hour,
SuccessURL: "/",
AllowRedirects: true,
RedirectQueryParameter: "backTo",
PreventExternalRedirects: true,
CheckRefererOnRedirects: true,
LoginPath: "/login",
CookieName: "cookiename",
CookieHTTPOnly: true,
Backends: login.Options{
"simple": map[string]string{
"bob": "secret",
@@ -133,12 +153,16 @@ func TestSetup(t *testing.T) {
}`,
shouldErr: false,
config: login.Config{
JwtSecret: "jwtsecret",
JwtExpiry: 24 * time.Hour,
SuccessURL: "/",
LoginPath: "/login",
CookieName: "jwt_token",
CookieHTTPOnly: true,
JwtSecret: "jwtsecret",
JwtExpiry: 24 * time.Hour,
SuccessURL: "/",
AllowRedirects: true,
RedirectQueryParameter: "backTo",
PreventExternalRedirects: true,
CheckRefererOnRedirects: true,
LoginPath: "/login",
CookieName: "jwt_token",
CookieHTTPOnly: true,
Backends: login.Options{
"simple": map[string]string{
"bob": "secret",
@@ -178,7 +202,7 @@ func TestSetup_RelativeTemplateFile(t *testing.T) {
caddyfile := "loginsrv {\n template myTemplate.tpl\n simple bob=secret\n}"
root, _ := ioutil.TempDir("", "")
expectedPath := filepath.FromSlash(root + "/myTemplate.tpl")
c := caddy.NewTestController("http", caddyfile)
c.Key = "RelativeTemplateFileTest"
config := httpserver.GetConfig(c)
+6
View File
@@ -31,8 +31,10 @@ func DefaultConfig() *Config {
JwtRefreshes: 0,
SuccessURL: "/",
AllowRedirects: true,
RedirectQueryParameter: "backTo",
PreventExternalRedirects: true,
CheckRefererOnRedirects: true,
WhitelistDomainsFile: "",
LogoutURL: "",
LoginPath: "/login",
CookieName: "jwt_token",
@@ -56,8 +58,10 @@ type Config struct {
JwtRefreshes int
SuccessURL string
AllowRedirects bool
RedirectQueryParameter string
PreventExternalRedirects bool
CheckRefererOnRedirects bool
WhitelistDomainsFile string
LogoutURL string
Template string
LoginPath string
@@ -111,8 +115,10 @@ func (c *Config) ConfigureFlagSet(f *flag.FlagSet) {
f.StringVar(&c.CookieDomain, "cookie-domain", c.CookieDomain, "The optional domain parameter for the cookie")
f.StringVar(&c.SuccessURL, "success-url", c.SuccessURL, "The url to redirect after login")
f.BoolVar(&c.AllowRedirects, "allow-redirects", c.AllowRedirects, "Allow dynamic redirects by parameter")
f.StringVar(&c.RedirectQueryParameter, "redirect-query-parameter", c.RedirectQueryParameter, "Allow dynamic redirects by parameter")
f.BoolVar(&c.PreventExternalRedirects, "prevent-external-redirects", c.PreventExternalRedirects, "Prevent dynamic redirects from redirecting to an external domain")
f.BoolVar(&c.CheckRefererOnRedirects, "check-referer-on-redirects", c.CheckRefererOnRedirects, "When redirecting check that the referer is the same domain")
f.StringVar(&c.WhitelistDomainsFile, "whitelist-domains-file", c.WhitelistDomainsFile, "the file containing a list of domains that redirects are allowed to, one domain per line")
f.StringVar(&c.LogoutURL, "logout-url", c.LogoutURL, "The url or path to redirect after logout")
f.StringVar(&c.Template, "template", c.Template, "An alternative template for the login form")
+50 -29
View File
@@ -2,10 +2,11 @@ package login
import (
"flag"
. "github.com/stretchr/testify/assert"
"os"
"testing"
"time"
. "github.com/stretchr/testify/assert"
)
func TestConfig_ReadConfigDefaults(t *testing.T) {
@@ -28,6 +29,11 @@ func TestConfig_ReadConfig(t *testing.T) {
"--jwt-secret=jwtsecret",
"--jwt-expiry=42h42m",
"--success-url=successurl",
"--allow-redirects=true",
"--redirect-query-parameter=backTo",
"--prevent-external-redirects=true",
"--check-referer-on-redirects=true",
"--whitelist-domains-file=File",
"--logout-url=logouturl",
"--template=template",
"--login-path=loginpath",
@@ -42,20 +48,25 @@ func TestConfig_ReadConfig(t *testing.T) {
}
expected := &Config{
Host: "host",
Port: "port",
LogLevel: "loglevel",
TextLogging: true,
JwtSecret: "jwtsecret",
JwtExpiry: 42*time.Hour + 42*time.Minute,
SuccessURL: "successurl",
LogoutURL: "logouturl",
Template: "template",
LoginPath: "loginpath",
CookieName: "cookiename",
CookieExpiry: 23 * time.Minute,
CookieDomain: "*.example.com",
CookieHTTPOnly: false,
Host: "host",
Port: "port",
LogLevel: "loglevel",
TextLogging: true,
JwtSecret: "jwtsecret",
JwtExpiry: 42*time.Hour + 42*time.Minute,
SuccessURL: "successurl",
AllowRedirects: true,
RedirectQueryParameter: "backTo",
PreventExternalRedirects: true,
CheckRefererOnRedirects: true,
WhitelistDomainsFile: "File",
LogoutURL: "logouturl",
Template: "template",
LoginPath: "loginpath",
CookieName: "cookiename",
CookieExpiry: 23 * time.Minute,
CookieDomain: "*.example.com",
CookieHTTPOnly: false,
Backends: Options{
"simple": map[string]string{},
"foo": map[string]string{},
@@ -82,6 +93,11 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) {
NoError(t, os.Setenv("LOGINSRV_JWT_SECRET", "jwtsecret"))
NoError(t, os.Setenv("LOGINSRV_JWT_EXPIRY", "42h42m"))
NoError(t, os.Setenv("LOGINSRV_SUCCESS_URL", "successurl"))
NoError(t, os.Setenv("LOGINSRV_ALLOW_REDIRECTS", "true"))
NoError(t, os.Setenv("LOGINSRV_REDIRECT_QUERY_PARAMETER", "backTo"))
NoError(t, os.Setenv("LOGINSRV_PREVENT_EXTERNAL_REDIRECTS", "true"))
NoError(t, os.Setenv("LOGINSRV_CHECK_REFERER_ON_REDIRECTS", "true"))
NoError(t, os.Setenv("LOGINSRV_WHITELIST_DOMAINS_FILE", "File"))
NoError(t, os.Setenv("LOGINSRV_LOGOUT_URL", "logouturl"))
NoError(t, os.Setenv("LOGINSRV_TEMPLATE", "template"))
NoError(t, os.Setenv("LOGINSRV_LOGIN_PATH", "loginpath"))
@@ -94,20 +110,25 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) {
NoError(t, os.Setenv("LOGINSRV_GRACE_PERIOD", "4s"))
expected := &Config{
Host: "host",
Port: "port",
LogLevel: "loglevel",
TextLogging: true,
JwtSecret: "jwtsecret",
JwtExpiry: 42*time.Hour + 42*time.Minute,
SuccessURL: "successurl",
LogoutURL: "logouturl",
Template: "template",
LoginPath: "loginpath",
CookieName: "cookiename",
CookieExpiry: 23 * time.Minute,
CookieDomain: "*.example.com",
CookieHTTPOnly: false,
Host: "host",
Port: "port",
LogLevel: "loglevel",
TextLogging: true,
JwtSecret: "jwtsecret",
JwtExpiry: 42*time.Hour + 42*time.Minute,
SuccessURL: "successurl",
AllowRedirects: true,
RedirectQueryParameter: "backTo",
PreventExternalRedirects: true,
CheckRefererOnRedirects: true,
WhitelistDomainsFile: "File",
LogoutURL: "logouturl",
Template: "template",
LoginPath: "loginpath",
CookieName: "cookiename",
CookieExpiry: 23 * time.Minute,
CookieDomain: "*.example.com",
CookieHTTPOnly: false,
Backends: Options{
"simple": map[string]string{
"foo": "bar",
+20 -87
View File
@@ -69,39 +69,28 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if (h.shouldRedirect(r)) && (r.Method != "POST") {
queries, _ := url.ParseQuery(r.URL.RawQuery)
if queries.Get(h.config.RedirectQueryParameter) != "" {
cookie := http.Cookie{
Name: h.config.RedirectQueryParameter,
Value: queries.Get(h.config.RedirectQueryParameter),
}
spew.Dump(cookie)
http.SetCookie(w, &cookie)
}
}
_, err := h.oauth.GetConfigFromRequest(r)
if err == nil {
h.handleOauth(w, r)
return
}
if h.shouldSetCookie(r) {
queries, _ := url.ParseQuery(r.URL.RawQuery)
cookie := http.Cookie{Name: "redirect_url", Value: queries.Get("backTo")}
http.SetCookie(w, &cookie)
}
h.handleLogin(w, r)
return
}
func (h *Handler) shouldSetCookie(r *http.Request) bool {
if h.config.AllowRedirects {
if h.config.CheckRefererOnRedirects {
referer, _ := url.Parse(r.Header.Get("Referer"))
if referer.Host != r.Host {
logging.Application(r.Header).Warnf(
"Referer domain: '%s' does not match current domain '%s'",
referer.Host,
r.Host,
)
return false
}
}
return true
}
return false
}
func (h *Handler) handleOauth(w http.ResponseWriter, r *http.Request) {
startedFlow, authenticated, userInfo, err := h.oauth.Handle(w, r)
@@ -143,7 +132,7 @@ func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
if r.Method == "DELETE" || r.FormValue("logout") == "true" {
h.deleteToken(w)
h.deleteCookie(w, h.config.CookieName)
if h.config.LogoutURL != "" {
w.Header().Set("Location", h.config.LogoutURL)
w.WriteHeader(303)
@@ -218,9 +207,9 @@ func (h *Handler) handleRefresh(w http.ResponseWriter, r *http.Request, userInfo
}
}
func (h *Handler) deleteToken(w http.ResponseWriter) {
func (h *Handler) deleteCookie(w http.ResponseWriter, cookieName string) {
cookie := &http.Cookie{
Name: h.config.CookieName,
Name: cookieName,
Value: "delete",
HttpOnly: true,
Expires: time.Unix(0, 0),
@@ -257,10 +246,11 @@ func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, u
http.SetCookie(w, cookie)
//redirectURL := h.config.SuccessURL
//fmt.Printf("redirectURL is: %s\n", h.redirectURL(r))
w.Header().Set("Location", h.redirectURL(r))
w.Header().Set("Location", h.redirectURL(r, w))
_, err := r.Cookie(h.config.RedirectQueryParameter)
if err == nil {
h.deleteCookie(w, h.config.RedirectQueryParameter)
}
w.WriteHeader(303)
return
}
@@ -270,63 +260,6 @@ func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, u
fmt.Fprintf(w, "%s", token)
}
func (h *Handler) redirectURL(r *http.Request) string {
origin, _ := url.Parse(r.Header.Get("Origin"))
originScheme := origin.Scheme
originHost := origin.Host
redirectURL := fmt.Sprintf("%s://%s%s", originScheme, originHost, h.config.SuccessURL)
if h.config.AllowRedirects {
cookie, _ := r.Cookie("redirect_url")
parsedURL, err := url.Parse(cookie.Value)
if err != nil {
spew.Dump("oops can't parse that url")
}
scheme := parsedURL.Scheme
host := parsedURL.Host
path := parsedURL.Path
//spew.Dump(scheme)
//spew.Dump(host)
//spew.Dump(path)
if scheme == "" {
scheme = originScheme
}
if host == "" {
host = originHost
} else {
if h.config.PreventExternalRedirects {
if host != originHost {
logging.Application(r.Header).Warnf(
"Redirect attempt to '%s' but -prevent-external-redirects is set to true",
host,
)
return redirectURL
} else {
host = originHost
}
} else {
//TODO read domains from file
domains := "2google.com"
if host != domains {
logging.Application(r.Header).Warnf(
"Redirect attempt to '%s' but it is not in domain whitelist",
host,
)
return redirectURL
}
}
}
if path == "" {
path = h.config.SuccessURL
}
redirectURL = fmt.Sprintf("%s://%s%s", scheme, host, path)
}
return redirectURL
}
func (h *Handler) createToken(userInfo jwt.Claims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS512, userInfo)
return token.SignedString([]byte(h.config.JwtSecret))
+113 -4
View File
@@ -3,22 +3,27 @@ package login
import (
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
. "github.com/stretchr/testify/assert"
"github.com/tarent/loginsrv/model"
"github.com/tarent/loginsrv/oauth2"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
. "github.com/stretchr/testify/assert"
"github.com/tarent/loginsrv/model"
"github.com/tarent/loginsrv/oauth2"
)
const TypeJSON = "Content-Type: application/json"
const TypeForm = "Content-Type: application/x-www-form-urlencoded"
const AcceptHTML = "Accept: text/html"
const AcceptJwt = "Accept: application/jwt"
const Host = "Host: example.com"
const BadReferer = "Referer: http://evildomain.com"
func testConfig() *Config {
testConfig := DefaultConfig()
@@ -238,6 +243,110 @@ func TestHandler_LoginWeb(t *testing.T) {
Equal(t, recorder.Header().Get("Set-Cookie"), "")
}
func TestHandler_Redirect(t *testing.T) {
//by default set redirect_cookie
recorder := call(req("GET", "/context/login?backTo=/website", "", TypeForm, AcceptHTML))
setCookieList := readSetCookies(recorder.Header())
Equal(t, 1, len(setCookieList))
cookie := setCookieList[0]
Equal(t, "backTo", cookie.Name)
Equal(t, "/website", cookie.Value)
//by default allowed redirects
recorder = call(req("POST", "/context/login?backTo=/website", "username=bob&password=secret", TypeForm, AcceptHTML))
Equal(t, 303, recorder.Code)
Equal(t, "/website", recorder.Header().Get("Location"))
//redirect to SuccessURL if AllowRedirects is false
cfg := DefaultConfig()
cfg.AllowRedirects = false
h := &Handler{
backends: []Backend{
NewSimpleBackend(map[string]string{"bob": "secret"}),
},
oauth: oauth2.NewManager(),
config: cfg,
}
recorder = httptest.NewRecorder()
h.ServeHTTP(recorder, req("POST", "/login?backTo=/website", "username=bob&password=secret", TypeForm, AcceptHTML))
Equal(t, 303, recorder.Code)
Equal(t, "/", recorder.Header().Get("Location"))
//by default don't set redirect cookie if Referer doesn't match origin
recorder = call(req("GET", "/context/login?backTo=/website", "", TypeForm, AcceptHTML, BadReferer))
setCookieList = readSetCookies(recorder.Header())
Equal(t, 0, len(setCookieList))
//don't set redirect cookie if referrer is malformed
recorder = call(req("GET", "/context/login?backTo=/website", "", TypeForm, AcceptHTML, "Referer: :notvalid"))
setCookieList = readSetCookies(recorder.Header())
Equal(t, 0, len(setCookieList))
//set redirect cookie with mismatch referer if CheckRefererOnRedirects is false
cfg = DefaultConfig()
cfg.CheckRefererOnRedirects = false
h = &Handler{
backends: []Backend{
NewSimpleBackend(map[string]string{"bob": "secret"}),
},
oauth: oauth2.NewManager(),
config: cfg,
}
recorder = httptest.NewRecorder()
h.ServeHTTP(recorder, req("GET", "/login?backTo=/website", "", TypeForm, AcceptHTML, BadReferer))
setCookieList = readSetCookies(recorder.Header())
Equal(t, 1, len(setCookieList))
cookie = setCookieList[0]
Equal(t, "backTo", cookie.Name)
Equal(t, "/website", cookie.Value)
//by default prevent redirect to external site
recorder = call(req("POST", "/context/login?backTo=//evildomain.com/phishing.html", "username=bob&password=secret", TypeForm, AcceptHTML))
Equal(t, 303, recorder.Code)
Equal(t, "/", recorder.Header().Get("Location"))
//by default if the parsed path is empty redirect to SuccessURL
recorder = call(req("POST", "/context/login?backTo=https://evildomain.com", "username=bob&password=secret", TypeForm, AcceptHTML))
Equal(t, 303, recorder.Code)
Equal(t, "/", recorder.Header().Get("Location"))
//redirect to success url if domains whitelist file doesn't exist
cfg = DefaultConfig()
cfg.PreventExternalRedirects = false
cfg.WhitelistDomainsFile = "domains_whitelist.txt"
h = &Handler{
backends: []Backend{
NewSimpleBackend(map[string]string{"bob": "secret"}),
},
oauth: oauth2.NewManager(),
config: cfg,
}
recorder = httptest.NewRecorder()
h.ServeHTTP(recorder, req("POST", "/login?backTo=https://gooddomain.com/website", "username=bob&password=secret", TypeForm, AcceptHTML, BadReferer))
Equal(t, 303, recorder.Code)
Equal(t, "/", recorder.Header().Get("Location"))
//setup domain whitelist file
d1 := []byte("gooddomain.com\n")
_ = ioutil.WriteFile("domains_whitelist.txt", d1, 0644)
//allow redirect to domains on whitelist
recorder = httptest.NewRecorder()
h.ServeHTTP(recorder, req("POST", "/login?backTo=https://gooddomain.com/website", "username=bob&password=secret", TypeForm, AcceptHTML, BadReferer))
Equal(t, 303, recorder.Code)
Equal(t, "https://gooddomain.com/website", recorder.Header().Get("Location"))
//allow redirect to domains on whitelist
recorder = httptest.NewRecorder()
h.ServeHTTP(recorder, req("POST", "/login?backTo=https://evildomain.com/website", "username=bob&password=secret", TypeForm, AcceptHTML, BadReferer))
Equal(t, 303, recorder.Code)
Equal(t, "/", recorder.Header().Get("Location"))
//remove domains whitelist file
err := os.Remove("domains_whitelist.txt")
Equal(t, nil, err)
}
func TestHandler_Refresh(t *testing.T) {
h := testHandler()
input := model.UserInfo{Sub: "bob", Expiry: time.Now().Add(time.Second).Unix()}
+119
View File
@@ -0,0 +1,119 @@
package login
import (
"bufio"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"github.com/tarent/loginsrv/logging"
)
func (h *Handler) shouldRedirect(r *http.Request) bool {
if h.config.AllowRedirects {
if h.config.CheckRefererOnRedirects {
referer, err := url.Parse(r.Header.Get("Referer"))
if err != nil {
logging.Application(r.Header).Warnf(
"couldn't parse redirect url %s",
err,
)
return false
}
if referer.Host != r.Host {
logging.Application(r.Header).Warnf(
"Referer domain: '%s' does not match current domain '%s'",
referer.Host,
r.Host,
)
return false
}
}
return true
}
return false
}
func (h *Handler) redirectURL(r *http.Request, w http.ResponseWriter) string {
if h.config.AllowRedirects {
parsedURL, err := h.parseURL(r)
if err != nil {
logging.Application(r.Header).Warnf(
"error parsing redict URL: %s",
err,
)
return h.config.SuccessURL
}
if h.config.PreventExternalRedirects {
if parsedURL.Path == "" {
return h.config.SuccessURL
} else {
if (parsedURL.Host != "") && (r.Host != parsedURL.Host) {
logging.Application(r.Header).Warnf(
"Attempted redirect to %s",
parsedURL.Host,
)
return h.config.SuccessURL
}
return parsedURL.Path
}
} else {
if h.checkWhiteListDomains(r, parsedURL.Host) {
return fmt.Sprintf(
"%s://%s%s",
parsedURL.Scheme,
parsedURL.Host,
parsedURL.Path,
)
} else {
return h.config.SuccessURL
}
}
}
return h.config.SuccessURL
}
func (h *Handler) parseURL(r *http.Request) (*url.URL, error) {
cookie, err := r.Cookie(h.config.RedirectQueryParameter)
if err != nil {
//try reading parameter as it might be a POST request and so not have set the cookie yet
queries, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return nil, err
}
if queries.Get(h.config.RedirectQueryParameter) != "" {
parsedURL, err := url.Parse(queries.Get(h.config.RedirectQueryParameter))
return parsedURL, err
} else {
return nil, errors.New("no redirect")
}
}
parsedURL, err := url.Parse(cookie.Value)
return parsedURL, err
}
func (h *Handler) checkWhiteListDomains(r *http.Request, host string) bool {
f, err := os.Open(h.config.WhitelistDomainsFile)
defer f.Close()
if err != nil {
logging.Application(r.Header).Warnf(
"can't open domains file '%s'",
h.config.WhitelistDomainsFile,
)
return false
}
scanner := bufio.NewScanner(f)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
if host == scanner.Text() {
return true
}
}
logging.Application(r.Header).Warnf(
"Domain '%s' not in whitelist",
host,
)
return false
}