mirror of
https://github.com/wahyd4/loginsrv.git
synced 2026-08-09 04:46:29 +10:00
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:
@@ -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`.
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user