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
cace4f18a6
commit
febc3e4fe3
194
main.go
194
main.go
|
@ -6,6 +6,9 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/golang/groupcache/lru"
|
||||
"github.com/namsral/flag"
|
||||
"github.com/pborman/uuid"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
@ -13,65 +16,76 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/groupcache/lru"
|
||||
"github.com/namsral/flag"
|
||||
"github.com/pborman/uuid"
|
||||
)
|
||||
|
||||
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
|
||||
AllowAllPtr *bool
|
||||
}
|
||||
|
||||
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")
|
||||
allowAllPtr := flag.Bool("allow-all", false, "allow all discourse users (default: admin users only)")
|
||||
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.AllowAllPtr = flag.Bool("allow-all", false, "allow all discourse users (default: admin users only)")
|
||||
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, *allowAllPtr)
|
||||
handler := authProxyHandler(proxy, config)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: *listenUriPtr,
|
||||
Addr: *config.ListenUriPtr,
|
||||
Handler: handler,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
|
@ -81,69 +95,95 @@ func main() {
|
|||
log.Fatal(server.ListenAndServe())
|
||||
}
|
||||
|
||||
func redirectIfCookieMissing(handler http.Handler, ssoSecret, cookieSecret, ssoUri, proxyHost string, allowAll bool) http.Handler {
|
||||
func authProxyHandler(handler http.Handler, config *Config) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie("__discourse_proxy")
|
||||
|
||||
var username string
|
||||
|
||||
if err == nil && cookie != nil {
|
||||
username, err = parseCookie(cookie.Value, cookieSecret)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
r.Header.Set("Discourse-User-Name", username)
|
||||
handler.ServeHTTP(w, r)
|
||||
if checkAuthorizationHeader(handler, r, w, config) {
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
sso := query.Get("sso")
|
||||
sig := query.Get("sig")
|
||||
|
||||
if len(sso) == 0 {
|
||||
url := ssoUri + "/session/sso_provider?" + sso_payload(ssoSecret, proxyHost, r.URL.String())
|
||||
http.Redirect(w, r, url, 302)
|
||||
} else {
|
||||
decoded, _ := base64.StdEncoding.DecodeString(sso)
|
||||
decodedString := string(decoded)
|
||||
parsedQuery, _ := url.ParseQuery(decodedString)
|
||||
|
||||
username := parsedQuery["username"]
|
||||
admin := parsedQuery["admin"]
|
||||
nonce := parsedQuery["nonce"]
|
||||
|
||||
if len(nonce) > 0 && len(username) > 0 {
|
||||
|
||||
if allowAll == false {
|
||||
if len(admin) < 1 || admin[0] != "true" {
|
||||
log.Println("Rejecting access to non-admin user ", username)
|
||||
w.Write([]byte(fmt.Sprintf("auth-proxy access is restricted to admin users, and %s is not an admin", username)))
|
||||
return
|
||||
}
|
||||
log.Println("Granting access to admin user ", username)
|
||||
}
|
||||
|
||||
returnUrl, err := getReturnUrl(ssoSecret, sso, sig, nonce[0])
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
// 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}
|
||||
http.SetCookie(w, &cookie)
|
||||
// works around weird safari stuff
|
||||
fmt.Fprintf(w, "<html><head></head><body><script>window.location = '%v'</script></body>", returnUrl)
|
||||
}
|
||||
}
|
||||
|
||||
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, config.CookieSecret)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
r.Header.Set(*config.UsernameHeaderPtr, username)
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
sso := query.Get("sso")
|
||||
sig := query.Get("sig")
|
||||
|
||||
if len(sso) == 0 {
|
||||
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)
|
||||
decodedString := string(decoded)
|
||||
parsedQuery, _ := url.ParseQuery(decodedString)
|
||||
|
||||
username := parsedQuery["username"]
|
||||
admin := parsedQuery["admin"]
|
||||
nonce := parsedQuery["nonce"]
|
||||
|
||||
if len(nonce) > 0 && len(admin) > 0 && len(username) > 0 && admin[0] == "true" {
|
||||
returnUrl, err := getReturnUrl(*config.SsoSecretPtr, sso, sig, nonce[0])
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
// we have a valid auth
|
||||
expiration := time.Now().Add(365 * 24 * time.Hour)
|
||||
|
||||
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) {
|
||||
value, gotNonce := nonceCache.Get(nonce)
|
||||
returnUrl = value.(string)
|
||||
|
|
Loading…
Reference in New Issue