Support HTTP basic auth, allow username header name to be overridden
The big change here is to support an extremely limited form of HTTP basic auth, for those situations when you've got some subset of requests coming in which still need to be authenticated, but which aren't able to authenticate via Discourse SSO. The intended use case is for webhooks and other progammatic access methods. It is not intended to be a fully-featured HTTP auth method (it only supports a single hard-coded user/password pair), but instead an extremely simplistic "escape hatch". If you need more complicated HTTP authentication, you probably want to install nginx and do some crazy proxy chain games. Best of luck to you with that. To avoid getting in the way of the SSO flow, the HTTP authentication is done "blind"; that is, a `WWW-Authenticate` is never sent in a response. This may get up the nose of some user agents, however I can't see an easy way around this. Allowing the username header to be changed to something other than Discourse-User-Name is a smaller change, needed to support third-party software which looks for the authenticated username in a different header, and which can't be overridden without a hammer and chisel.
This commit is contained in:
parent
cfa7d348a2
commit
c52cfa2a6b
99
main.go
99
main.go
|
@ -6,9 +6,9 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/golang/groupcache/lru"
|
||||
"github.com/namsral/flag"
|
||||
"github.com/pborman/uuid"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
@ -20,56 +20,70 @@ import (
|
|||
|
||||
var nonceCache = lru.New(20)
|
||||
|
||||
func main() {
|
||||
type Config struct {
|
||||
ListenUriPtr *string
|
||||
ProxyUriPtr *string
|
||||
OriginUriPtr *string
|
||||
SsoSecretPtr *string
|
||||
SsoUriPtr *string
|
||||
BasicAuthPtr *string
|
||||
UsernameHeaderPtr *string
|
||||
CookieSecret string
|
||||
}
|
||||
|
||||
listenUriPtr := flag.String("listen-url", "", "uri to listen on eg: localhost:2001. leave blank to set equal to proxy-url")
|
||||
proxyUriPtr := flag.String("proxy-url", "", "outer url of this host eg: http://secrets.example.com")
|
||||
originUriPtr := flag.String("origin-url", "", "origin to proxy eg: http://localhost:2002")
|
||||
ssoSecretPtr := flag.String("sso-secret", "", "SSO secret for origin")
|
||||
ssoUriPtr := flag.String("sso-url", "", "SSO endpoint eg: http://discourse.forum.com")
|
||||
func main() {
|
||||
config := new(Config)
|
||||
|
||||
config.ListenUriPtr = flag.String("listen-url", "", "uri to listen on eg: localhost:2001. leave blank to set equal to proxy-url")
|
||||
config.ProxyUriPtr = flag.String("proxy-url", "", "outer url of this host eg: http://secrets.example.com")
|
||||
config.OriginUriPtr = flag.String("origin-url", "", "origin to proxy eg: http://localhost:2002")
|
||||
config.SsoSecretPtr = flag.String("sso-secret", "", "SSO secret for origin")
|
||||
config.SsoUriPtr = flag.String("sso-url", "", "SSO endpoint eg: http://discourse.forum.com")
|
||||
config.BasicAuthPtr = flag.String("basic-auth", "", "HTTP Basic authentication credentials to let through directly")
|
||||
config.UsernameHeaderPtr = flag.String("username-header", "Discourse-User-Name", "Request header to pass authenticated username into")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
originUrl, err := url.Parse(*originUriPtr)
|
||||
originUrl, err := url.Parse(*config.OriginUriPtr)
|
||||
|
||||
if err != nil {
|
||||
flag.Usage()
|
||||
log.Fatal("invalid origin url")
|
||||
}
|
||||
|
||||
_, err = url.Parse(*ssoUriPtr)
|
||||
_, err = url.Parse(*config.SsoUriPtr)
|
||||
|
||||
if err != nil {
|
||||
flag.Usage()
|
||||
log.Fatal("invalid sso url, should point at Discourse site with enable sso")
|
||||
}
|
||||
|
||||
proxyUrl, err2 := url.Parse(*proxyUriPtr)
|
||||
proxyUrl, err2 := url.Parse(*config.ProxyUriPtr)
|
||||
|
||||
if err2 != nil {
|
||||
flag.Usage()
|
||||
log.Fatal("invalid proxy uri")
|
||||
}
|
||||
|
||||
if *listenUriPtr == "" {
|
||||
if *config.ListenUriPtr == "" {
|
||||
log.Println("Defaulting to listening on the proxy url")
|
||||
*listenUriPtr = proxyUrl.Host
|
||||
*config.ListenUriPtr = proxyUrl.Host
|
||||
}
|
||||
|
||||
if *proxyUriPtr == "" || *originUriPtr == "" || *ssoSecretPtr == "" || *ssoUriPtr == "" || *listenUriPtr == "" {
|
||||
if *config.ProxyUriPtr == "" || *config.OriginUriPtr == "" || *config.SsoSecretPtr == "" || *config.SsoUriPtr == "" || *config.ListenUriPtr == "" {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
cookieSecret := uuid.New()
|
||||
config.CookieSecret = uuid.New()
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(originUrl)
|
||||
|
||||
handler := redirectIfCookieMissing(proxy, *ssoSecretPtr, cookieSecret, *ssoUriPtr, *proxyUriPtr)
|
||||
handler := authProxyHandler(proxy, config)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: *listenUriPtr,
|
||||
Addr: *config.ListenUriPtr,
|
||||
Handler: handler,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
|
@ -79,18 +93,56 @@ func main() {
|
|||
log.Fatal(server.ListenAndServe())
|
||||
}
|
||||
|
||||
func redirectIfCookieMissing(handler http.Handler, ssoSecret, cookieSecret, ssoUri, proxyHost string) http.Handler {
|
||||
func authProxyHandler(handler http.Handler, config *Config) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if checkAuthorizationHeader(handler, r, w, config) {
|
||||
return
|
||||
}
|
||||
redirectIfNoCookie(handler, r, w, config)
|
||||
})
|
||||
}
|
||||
|
||||
func checkAuthorizationHeader(handler http.Handler, r *http.Request, w http.ResponseWriter, config *Config) bool {
|
||||
if *config.BasicAuthPtr == "" {
|
||||
// Can't auth if we don't have anything to auth against
|
||||
return false
|
||||
}
|
||||
|
||||
auth_header := r.Header.Get("Authorization")
|
||||
if len(auth_header) < 6 {
|
||||
return false
|
||||
}
|
||||
|
||||
if auth_header[0:6] == "Basic " {
|
||||
b_creds, _ := base64.StdEncoding.DecodeString(auth_header[6:])
|
||||
creds := string(b_creds)
|
||||
if creds == *config.BasicAuthPtr {
|
||||
colon_idx := strings.Index(creds, ":")
|
||||
if colon_idx == -1 {
|
||||
return false
|
||||
}
|
||||
username := creds[0:colon_idx]
|
||||
r.Header.Set(*config.UsernameHeaderPtr, username)
|
||||
r.Header.Del("Authorization")
|
||||
handler.ServeHTTP(w, r)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func redirectIfNoCookie(handler http.Handler, r *http.Request, w http.ResponseWriter, config *Config) {
|
||||
cookie, err := r.Cookie("__discourse_proxy")
|
||||
|
||||
var username string
|
||||
|
||||
if err == nil && cookie != nil {
|
||||
username, err = parseCookie(cookie.Value, cookieSecret)
|
||||
username, err = parseCookie(cookie.Value, config.CookieSecret)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
r.Header.Set("Discourse-User-Name", username)
|
||||
r.Header.Set(*config.UsernameHeaderPtr, username)
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
@ -100,7 +152,7 @@ func redirectIfCookieMissing(handler http.Handler, ssoSecret, cookieSecret, ssoU
|
|||
sig := query.Get("sig")
|
||||
|
||||
if len(sso) == 0 {
|
||||
url := ssoUri + "/session/sso_provider?" + sso_payload(ssoSecret, proxyHost, r.URL.String())
|
||||
url := *config.SsoUriPtr + "/session/sso_provider?" + sso_payload(*config.SsoSecretPtr, *config.ProxyUriPtr, r.URL.String())
|
||||
http.Redirect(w, r, url, 302)
|
||||
} else {
|
||||
decoded, _ := base64.StdEncoding.DecodeString(sso)
|
||||
|
@ -112,7 +164,7 @@ func redirectIfCookieMissing(handler http.Handler, ssoSecret, cookieSecret, ssoU
|
|||
nonce := parsedQuery["nonce"]
|
||||
|
||||
if len(nonce) > 0 && len(admin) > 0 && len(username) > 0 && admin[0] == "true" {
|
||||
returnUrl, err := getReturnUrl(ssoSecret, sso, sig, nonce[0])
|
||||
returnUrl, err := getReturnUrl(*config.SsoSecretPtr, sso, sig, nonce[0])
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Invalid request")
|
||||
|
@ -122,14 +174,13 @@ func redirectIfCookieMissing(handler http.Handler, ssoSecret, cookieSecret, ssoU
|
|||
// we have a valid auth
|
||||
expiration := time.Now().Add(365 * 24 * time.Hour)
|
||||
|
||||
cookie := http.Cookie{Name: "__discourse_proxy", Value: signCookie(username[0], cookieSecret), Expires: expiration, HttpOnly: true}
|
||||
cookie := http.Cookie{Name: "__discourse_proxy", Value: signCookie(username[0], config.CookieSecret), Expires: expiration, HttpOnly: true}
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
// works around weird safari stuff
|
||||
fmt.Fprintf(w, "<html><head></head><body><script>window.location = '%v'</script></body>", returnUrl)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func getReturnUrl(secret string, payload string, sig string, nonce string) (returnUrl string, err error) {
|
||||
|
|
Loading…
Reference in New Issue