Add auth host feature
Allow central host for use as base for redirect_uri Closes #3
This commit is contained in:
parent
b3716d401e
commit
d230572879
38
README.md
38
README.md
@ -24,10 +24,11 @@ The following configuration is supported:
|
||||
|-----------------------|------|-----------|
|
||||
|-client-id|string|*Google Client ID (required)|
|
||||
|-client-secret|string|*Google Client Secret (required)|
|
||||
|-secret|string|*Secret used for signing (required)|
|
||||
|-config|string|Path to config file|
|
||||
|-cookie-domains|string|Comma separated list of cookie domains|
|
||||
|-auth-host|string|Central auth login (see below)|
|
||||
|-cookie-domains|string|Comma separated list of cookie domains (see below)|
|
||||
|-cookie-name|string|Cookie Name (default "_forward_auth")|
|
||||
|-cookie-secret|string|*Cookie secret (required)|
|
||||
|-cookie-secure|bool|Use secure cookies (default true)|
|
||||
|-csrf-cookie-name|string|CSRF Cookie Name (default "_forward_auth_csrf")|
|
||||
|-direct|bool|Run in direct mode (use own hostname as oppose to <br>X-Forwarded-Host, used for testing/development)
|
||||
@ -55,6 +56,39 @@ For example, if cookie domain is `test.com` and a request comes in on `app1.test
|
||||
|
||||
Beware however, if using cookie domains whilst running multiple instances of traefik/traefik-forward-auth for the same domain, the cookies will clash. You can fix this by using the same `cookie-secret` in both instances, or using a different `cookie-name` on each.
|
||||
|
||||
## Operation Modes
|
||||
|
||||
#### Overlay
|
||||
|
||||
Overlay is the default operation mode, in this mode the authorisation endpoint is overlayed onto any domain. By default the `/_oauth` path is used, this can be customised using the `-url-path` option.
|
||||
|
||||
If a request comes in for `www.myapp.com/home` then the user will be redirected to the google login, following this they will be sent back to `www.myapp.com/_oauth`, where their token will be validated (this request will not be forwarded to your application). Following successful authoristion, the user will return to their originally requested url of `www.myapp.com/home`.
|
||||
|
||||
As the hostname in the `redirect_uri` is dynamically generated based on the orignal request, every hostname must be permitted in the Google OAuth console (e.g. `www.myappp.com` would need to be added in the above example)
|
||||
|
||||
#### Auth Host
|
||||
|
||||
This is an optional mode of operation that is useful when dealing with a large number of subdomains, it is activated by using the `-auth-host` config option.
|
||||
|
||||
For example, if you have a few applications: `app1.test.com`, `app2.test.com`, `appN.test.com`, adding every domain to Google's console can become laborious.
|
||||
To utilise an auth host, permit domain level cookies by setting the cookie domain to `test.com` then set the `auth-host` to: `auth.test.com`.
|
||||
|
||||
The user flow will then be:
|
||||
|
||||
1. Request to `app10.test.com/home/page`
|
||||
2. User redirected to Google login
|
||||
3. After Google login, user is redirected to `auth.test.com/_oauth`
|
||||
4. Token, user and CSRF cookie is validated, auth cookie is set to `test.com`
|
||||
5. User is redirected to `app10.test.com/home/page`
|
||||
6. Request is allowed
|
||||
|
||||
With this setup, only `auth.test.com` must be permitted in the Google console.
|
||||
|
||||
Two criteria must be met for an `auth-host` to be used:
|
||||
|
||||
1. Request matches given `cookie-domain`
|
||||
2. `auth-host` is also subdomain of same `cookie-domain`
|
||||
|
||||
## Copyright
|
||||
|
||||
2018 Thom Seddon
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
type ForwardAuth struct {
|
||||
Path string
|
||||
Lifetime time.Duration
|
||||
Secret []byte
|
||||
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
@ -29,10 +30,11 @@ type ForwardAuth struct {
|
||||
TokenURL *url.URL
|
||||
UserURL *url.URL
|
||||
|
||||
AuthHost string
|
||||
|
||||
CookieName string
|
||||
CookieDomains []CookieDomain
|
||||
CSRFCookieName string
|
||||
CookieSecret []byte
|
||||
CookieSecure bool
|
||||
|
||||
Domain []string
|
||||
@ -210,9 +212,30 @@ func (f *ForwardAuth) returnUrl(r *http.Request) string {
|
||||
|
||||
// Get oauth redirect uri
|
||||
func (f *ForwardAuth) redirectUri(r *http.Request) string {
|
||||
if use, _ := f.useAuthDomain(r); use {
|
||||
proto := r.Header.Get("X-Forwarded-Proto")
|
||||
return fmt.Sprintf("%s://%s%s", proto, f.AuthHost, f.Path)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s", f.redirectBase(r), f.Path)
|
||||
}
|
||||
|
||||
// Should we use auth host + what it is
|
||||
func (f *ForwardAuth) useAuthDomain(r *http.Request) (bool, string) {
|
||||
if f.AuthHost == "" {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Does the request match a given cookie domain?
|
||||
reqMatch, reqHost := f.matchCookieDomains(r.Header.Get("X-Forwarded-Host"))
|
||||
|
||||
// Do any of the auth hosts match a cookie domain?
|
||||
authMatch, authHost := f.matchCookieDomains(f.AuthHost)
|
||||
|
||||
// We need both to match the same domain
|
||||
return reqMatch && authMatch && reqHost == authHost, reqHost
|
||||
}
|
||||
|
||||
// Cookie methods
|
||||
|
||||
// Create an auth cookie
|
||||
@ -238,7 +261,7 @@ func (f *ForwardAuth) MakeCSRFCookie(r *http.Request, nonce string) *http.Cookie
|
||||
Name: f.CSRFCookieName,
|
||||
Value: nonce,
|
||||
Path: "/",
|
||||
Domain: f.cookieDomain(r),
|
||||
Domain: f.csrfCookieDomain(r),
|
||||
HttpOnly: true,
|
||||
Secure: f.CookieSecure,
|
||||
Expires: f.cookieExpiry(),
|
||||
@ -251,7 +274,7 @@ func (f *ForwardAuth) ClearCSRFCookie(r *http.Request) *http.Cookie {
|
||||
Name: f.CSRFCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Domain: f.cookieDomain(r),
|
||||
Domain: f.csrfCookieDomain(r),
|
||||
HttpOnly: true,
|
||||
Secure: f.CookieSecure,
|
||||
Expires: time.Now().Local().Add(time.Hour * -1),
|
||||
@ -297,22 +320,44 @@ func (f *ForwardAuth) cookieDomain(r *http.Request) string {
|
||||
host = r.Host
|
||||
}
|
||||
|
||||
// Remove port for matching
|
||||
p := strings.Split(host, ":")
|
||||
|
||||
// Check if any of the given cookie domains matches
|
||||
for _, domain := range f.CookieDomains {
|
||||
if domain.Match(p[0]) {
|
||||
return domain.Domain
|
||||
_, domain := f.matchCookieDomains(host)
|
||||
return domain
|
||||
}
|
||||
|
||||
// Cookie domain
|
||||
func (f *ForwardAuth) csrfCookieDomain(r *http.Request) string {
|
||||
var host string
|
||||
if use, domain := f.useAuthDomain(r); use {
|
||||
host = domain
|
||||
} else if f.Direct {
|
||||
host = r.Host
|
||||
} else {
|
||||
host = r.Header.Get("X-Forwarded-Host")
|
||||
}
|
||||
|
||||
// Remove port
|
||||
p := strings.Split(host, ":")
|
||||
return p[0]
|
||||
}
|
||||
|
||||
// Return matching cookie domain if exists
|
||||
func (f *ForwardAuth) matchCookieDomains(domain string) (bool, string) {
|
||||
// Remove port
|
||||
p := strings.Split(domain, ":")
|
||||
|
||||
for _, d := range f.CookieDomains {
|
||||
if d.Match(p[0]) {
|
||||
return true, d.Domain
|
||||
}
|
||||
}
|
||||
|
||||
return p[0]
|
||||
return false, p[0]
|
||||
}
|
||||
|
||||
// Create cookie hmac
|
||||
func (f *ForwardAuth) cookieSignature(r *http.Request, email, expires string) string {
|
||||
hash := hmac.New(sha256.New, f.CookieSecret)
|
||||
hash := hmac.New(sha256.New, f.Secret)
|
||||
hash.Write([]byte(f.cookieDomain(r)))
|
||||
hash.Write([]byte(email))
|
||||
hash.Write([]byte(expires))
|
||||
|
@ -84,6 +84,11 @@ func TestValidateEmail(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetLoginURL(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "http://example.com", nil)
|
||||
r.Header.Add("X-Forwarded-Proto", "http")
|
||||
r.Header.Add("X-Forwarded-Host", "example.com")
|
||||
r.Header.Add("X-Forwarded-Uri", "/hello")
|
||||
|
||||
fw = &ForwardAuth{
|
||||
Path: "/_oauth",
|
||||
ClientId: "idtest",
|
||||
@ -95,10 +100,6 @@ func TestGetLoginURL(t *testing.T) {
|
||||
Path: "/auth",
|
||||
},
|
||||
}
|
||||
r, _ := http.NewRequest("GET", "http://example.com", nil)
|
||||
r.Header.Add("X-Forwarded-Proto", "http")
|
||||
r.Header.Add("X-Forwarded-Host", "example.com")
|
||||
r.Header.Add("X-Forwarded-Uri", "/hello")
|
||||
|
||||
// Check url
|
||||
uri, err := url.Parse(fw.GetLoginURL(r, "nonce"))
|
||||
@ -125,14 +126,145 @@ func TestGetLoginURL(t *testing.T) {
|
||||
"state": []string{"nonce:http://example.com/hello"},
|
||||
}
|
||||
if !reflect.DeepEqual(qs, expectedQs) {
|
||||
t.Error("Incorrect login query string, expected:")
|
||||
t.Error(expectedQs)
|
||||
t.Error("Got:")
|
||||
t.Error(qs)
|
||||
t.Error("Incorrect login query string:")
|
||||
qsDiff(expectedQs, qs)
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// With Auth URL but no matching cookie domain
|
||||
// - will not use auth host
|
||||
//
|
||||
fw = &ForwardAuth{
|
||||
Path: "/_oauth",
|
||||
AuthHost: "auth.example.com",
|
||||
ClientId: "idtest",
|
||||
ClientSecret: "sectest",
|
||||
Scope: "scopetest",
|
||||
LoginURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "test.com",
|
||||
Path: "/auth",
|
||||
},
|
||||
}
|
||||
|
||||
// Check url
|
||||
uri, err = url.Parse(fw.GetLoginURL(r, "nonce"))
|
||||
if err != nil {
|
||||
t.Error("Error parsing login url:", err)
|
||||
}
|
||||
if uri.Scheme != "https" {
|
||||
t.Error("Expected login Scheme to be \"https\", got:", uri.Scheme)
|
||||
}
|
||||
if uri.Host != "test.com" {
|
||||
t.Error("Expected login Host to be \"test.com\", got:", uri.Host)
|
||||
}
|
||||
if uri.Path != "/auth" {
|
||||
t.Error("Expected login Path to be \"/auth\", got:", uri.Path)
|
||||
}
|
||||
|
||||
// Check query string
|
||||
qs = uri.Query()
|
||||
expectedQs = url.Values{
|
||||
"client_id": []string{"idtest"},
|
||||
"redirect_uri": []string{"http://example.com/_oauth"},
|
||||
"response_type": []string{"code"},
|
||||
"scope": []string{"scopetest"},
|
||||
"state": []string{"nonce:http://example.com/hello"},
|
||||
}
|
||||
if !reflect.DeepEqual(qs, expectedQs) {
|
||||
t.Error("Incorrect login query string:")
|
||||
qsDiff(expectedQs, qs)
|
||||
}
|
||||
|
||||
//
|
||||
// With correct Auth URL + cookie domain
|
||||
//
|
||||
cookieDomain := NewCookieDomain("example.com")
|
||||
fw = &ForwardAuth{
|
||||
Path: "/_oauth",
|
||||
AuthHost: "auth.example.com",
|
||||
ClientId: "idtest",
|
||||
ClientSecret: "sectest",
|
||||
Scope: "scopetest",
|
||||
LoginURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "test.com",
|
||||
Path: "/auth",
|
||||
},
|
||||
CookieDomains: []CookieDomain{*cookieDomain},
|
||||
}
|
||||
|
||||
// Check url
|
||||
uri, err = url.Parse(fw.GetLoginURL(r, "nonce"))
|
||||
if err != nil {
|
||||
t.Error("Error parsing login url:", err)
|
||||
}
|
||||
if uri.Scheme != "https" {
|
||||
t.Error("Expected login Scheme to be \"https\", got:", uri.Scheme)
|
||||
}
|
||||
if uri.Host != "test.com" {
|
||||
t.Error("Expected login Host to be \"test.com\", got:", uri.Host)
|
||||
}
|
||||
if uri.Path != "/auth" {
|
||||
t.Error("Expected login Path to be \"/auth\", got:", uri.Path)
|
||||
}
|
||||
|
||||
// Check query string
|
||||
qs = uri.Query()
|
||||
expectedQs = url.Values{
|
||||
"client_id": []string{"idtest"},
|
||||
"redirect_uri": []string{"http://auth.example.com/_oauth"},
|
||||
"response_type": []string{"code"},
|
||||
"scope": []string{"scopetest"},
|
||||
"state": []string{"nonce:http://example.com/hello"},
|
||||
}
|
||||
qsDiff(expectedQs, qs)
|
||||
if !reflect.DeepEqual(qs, expectedQs) {
|
||||
t.Error("Incorrect login query string:")
|
||||
qsDiff(expectedQs, qs)
|
||||
}
|
||||
|
||||
//
|
||||
// With Auth URL + cookie domain, but from different domain
|
||||
// - will not use auth host
|
||||
//
|
||||
r, _ = http.NewRequest("GET", "http://another.com", nil)
|
||||
r.Header.Add("X-Forwarded-Proto", "http")
|
||||
r.Header.Add("X-Forwarded-Host", "another.com")
|
||||
r.Header.Add("X-Forwarded-Uri", "/hello")
|
||||
|
||||
// Check url
|
||||
uri, err = url.Parse(fw.GetLoginURL(r, "nonce"))
|
||||
if err != nil {
|
||||
t.Error("Error parsing login url:", err)
|
||||
}
|
||||
if uri.Scheme != "https" {
|
||||
t.Error("Expected login Scheme to be \"https\", got:", uri.Scheme)
|
||||
}
|
||||
if uri.Host != "test.com" {
|
||||
t.Error("Expected login Host to be \"test.com\", got:", uri.Host)
|
||||
}
|
||||
if uri.Path != "/auth" {
|
||||
t.Error("Expected login Path to be \"/auth\", got:", uri.Path)
|
||||
}
|
||||
|
||||
// Check query string
|
||||
qs = uri.Query()
|
||||
expectedQs = url.Values{
|
||||
"client_id": []string{"idtest"},
|
||||
"redirect_uri": []string{"http://another.com/_oauth"},
|
||||
"response_type": []string{"code"},
|
||||
"scope": []string{"scopetest"},
|
||||
"state": []string{"nonce:http://another.com/hello"},
|
||||
}
|
||||
qsDiff(expectedQs, qs)
|
||||
if !reflect.DeepEqual(qs, expectedQs) {
|
||||
t.Error("Incorrect login query string:")
|
||||
qsDiff(expectedQs, qs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO
|
||||
// func TestExchangeCode(t *testing.T) {
|
||||
// }
|
||||
@ -145,9 +277,35 @@ func TestGetLoginURL(t *testing.T) {
|
||||
// func TestMakeCookie(t *testing.T) {
|
||||
// }
|
||||
|
||||
// func TestMakeCSRFCookie(t *testing.T) {
|
||||
// t.Log("TODO")
|
||||
// }
|
||||
func TestMakeCSRFCookie(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "http://app.example.com", nil)
|
||||
r.Header.Add("X-Forwarded-Host", "app.example.com")
|
||||
|
||||
// No cookie domain or auth url
|
||||
fw = &ForwardAuth{}
|
||||
c := fw.MakeCSRFCookie(r, "12345678901234567890123456789012")
|
||||
if c.Domain != "app.example.com" {
|
||||
t.Error("Cookie Domain should match request domain, got:", c.Domain)
|
||||
}
|
||||
|
||||
// With cookie domain but no auth url
|
||||
cookieDomain := NewCookieDomain("example.com")
|
||||
fw = &ForwardAuth{CookieDomains: []CookieDomain{*cookieDomain},}
|
||||
c = fw.MakeCSRFCookie(r, "12345678901234567890123456789012")
|
||||
if c.Domain != "app.example.com" {
|
||||
t.Error("Cookie Domain should match request domain, got:", c.Domain)
|
||||
}
|
||||
|
||||
// With cookie domain and auth url
|
||||
fw = &ForwardAuth{
|
||||
AuthHost: "auth.example.com",
|
||||
CookieDomains: []CookieDomain{*cookieDomain},
|
||||
}
|
||||
c = fw.MakeCSRFCookie(r, "12345678901234567890123456789012")
|
||||
if c.Domain != "example.com" {
|
||||
t.Error("Cookie Domain should match request domain, got:", c.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearCSRFCookie(t *testing.T) {
|
||||
fw = &ForwardAuth{}
|
||||
|
28
main.go
28
main.go
@ -37,6 +37,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get auth cookie
|
||||
c, err := r.Cookie(fw.CookieName)
|
||||
if err != nil {
|
||||
// Error indicates no cookie, generate nonce
|
||||
@ -130,33 +131,40 @@ func main() {
|
||||
flag.String(flag.DefaultConfigFlagname, "", "Path to config file")
|
||||
path := flag.String("url-path", "_oauth", "Callback URL")
|
||||
lifetime := flag.Int("lifetime", 43200, "Session length in seconds")
|
||||
secret := flag.String("secret", "", "*Secret used for signing (required)")
|
||||
authHost := flag.String("auth-host", "", "Central auth login")
|
||||
clientId := flag.String("client-id", "", "*Google Client ID (required)")
|
||||
clientSecret := flag.String("client-secret", "", "*Google Client Secret (required)")
|
||||
cookieName := flag.String("cookie-name", "_forward_auth", "Cookie Name")
|
||||
cSRFCookieName := flag.String("csrf-cookie-name", "_forward_auth_csrf", "CSRF Cookie Name")
|
||||
cookieDomainList := flag.String("cookie-domains", "", "Comma separated list of cookie domains") //todo
|
||||
cookieSecret := flag.String("cookie-secret", "", "*Cookie secret (required)")
|
||||
cookieSecret := flag.String("cookie-secret", "", "depreciated")
|
||||
cookieSecure := flag.Bool("cookie-secure", true, "Use secure cookies")
|
||||
domainList := flag.String("domain", "", "Comma separated list of email domains to allow")
|
||||
direct := flag.Bool("direct", false, "Run in direct mode (use own hostname as oppose to X-Forwarded-Host, used for testing/development)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Backwards compatability
|
||||
if *secret == "" && *cookieSecret != "" {
|
||||
*secret = *cookieSecret
|
||||
}
|
||||
|
||||
// Check for show stopper errors
|
||||
err := false
|
||||
stop := false
|
||||
if *clientId == "" {
|
||||
err = true
|
||||
stop = true
|
||||
log.Critical("client-id must be set")
|
||||
}
|
||||
if *clientSecret == "" {
|
||||
err = true
|
||||
stop = true
|
||||
log.Critical("client-secret must be set")
|
||||
}
|
||||
if *cookieSecret == "" {
|
||||
err = true
|
||||
log.Critical("cookie-secret must be set")
|
||||
if *secret == "" {
|
||||
stop = true
|
||||
log.Critical("secret must be set")
|
||||
}
|
||||
if err {
|
||||
if stop {
|
||||
return
|
||||
}
|
||||
|
||||
@ -178,6 +186,8 @@ func main() {
|
||||
fw = &ForwardAuth{
|
||||
Path: fmt.Sprintf("/%s", *path),
|
||||
Lifetime: time.Second * time.Duration(*lifetime),
|
||||
Secret: []byte(*secret),
|
||||
AuthHost: *authHost,
|
||||
|
||||
ClientId: *clientId,
|
||||
ClientSecret: *clientSecret,
|
||||
@ -201,7 +211,6 @@ func main() {
|
||||
CookieName: *cookieName,
|
||||
CSRFCookieName: *cSRFCookieName,
|
||||
CookieDomains: cookieDomains,
|
||||
CookieSecret: []byte(*cookieSecret),
|
||||
CookieSecure: *cookieSecure,
|
||||
|
||||
Domain: domain,
|
||||
@ -212,6 +221,7 @@ func main() {
|
||||
// Attach handler
|
||||
http.HandleFunc("/", handler)
|
||||
|
||||
log.Debugf("Staring with options: %#v", fw)
|
||||
log.Notice("Litening on :4181")
|
||||
log.Notice(http.ListenAndServe(":4181", nil))
|
||||
}
|
||||
|
16
main_test.go
16
main_test.go
@ -63,6 +63,22 @@ func newHttpRequest(uri string) *http.Request {
|
||||
return r
|
||||
}
|
||||
|
||||
func qsDiff(one, two url.Values) {
|
||||
for k, _ := range one {
|
||||
if two.Get(k) == "" {
|
||||
fmt.Printf("Key missing: %s\n", k)
|
||||
}
|
||||
if one.Get(k) != two.Get(k) {
|
||||
fmt.Printf("Value different for %s: expected: '%s' got: '%s'\n", k, one.Get(k), two.Get(k))
|
||||
}
|
||||
}
|
||||
for k, _ := range two {
|
||||
if one.Get(k) == "" {
|
||||
fmt.Printf("Extra key: %s\n", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler(t *testing.T) {
|
||||
fw = &ForwardAuth{
|
||||
Path: "_oauth",
|
||||
|
Loading…
x
Reference in New Issue
Block a user