1
0
Fork 0
mirror of https://github.com/Luzifer/nginx-sso.git synced 2024-10-18 07:34:22 +00:00
nginx-sso/main.go
Knut Ahlers 45f15de654
Work around missing URL parameters
when passing the URL with parameters in the `go=` parameter inside
nginx. This is caused by nginx not being able to escape ampersands which
then are parsed as parameters to the login handler instead of parameters
of the redirect URL.

There is a quite old ticket in nginx to implement proper escaping of URL
elements which would be a way better solution but until someone decides
to take care of that this should at least improve the situation.

refs #39

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2019-04-21 00:15:36 +02:00

270 lines
8.1 KiB
Go

package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/signal"
"path"
"syscall"
"github.com/flosch/pongo2"
"github.com/gorilla/context"
"github.com/gorilla/sessions"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
"github.com/Luzifer/nginx-sso/plugins"
"github.com/Luzifer/rconfig"
)
type mainConfig struct {
ACL acl `yaml:"acl"`
AuditLog auditLogger `yaml:"audit_log"`
Cookie struct {
Domain string `yaml:"domain"`
AuthKey string `yaml:"authentication_key"`
Expire int `yaml:"expire"`
Prefix string `yaml:"prefix"`
Secure bool `yaml:"secure"`
}
Listen struct {
Addr string `yaml:"addr"`
Port int `yaml:"port"`
} `yaml:"listen"`
Login struct {
Title string `yaml:"title"`
DefaultMethod string `yaml:"default_method"`
HideMFAField bool `yaml:"hide_mfa_field"`
Names map[string]string `yaml:"names"`
} `yaml:"login"`
Plugins struct {
Directory string `yaml:"directory"`
} `yaml:"plugins"`
}
func (m *mainConfig) GetSessionOpts() *sessions.Options {
return &sessions.Options{
Path: "/",
Domain: m.Cookie.Domain,
MaxAge: m.Cookie.Expire,
Secure: m.Cookie.Secure,
HttpOnly: true,
}
}
var (
cfg = struct {
ConfigFile string `flag:"config,c" default:"config.yaml" env:"CONFIG" description:"Location of the configuration file"`
LogLevel string `flag:"log-level" default:"info" description:"Level of logs to display (debug, info, warn, error)"`
TemplateDir string `flag:"frontend-dir" default:"./frontend/" env:"FRONTEND_DIR" description:"Location of the directory containing the web assets"`
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
}{}
mainCfg = mainConfig{}
cookieStore *sessions.CookieStore
version = "dev"
)
func init() {
if err := rconfig.Parse(&cfg); err != nil {
log.WithError(err).Fatal("Unable to parse commandline options")
}
if l, err := log.ParseLevel(cfg.LogLevel); err != nil {
log.WithError(err).Fatal("Unable to parse log level")
} else {
log.SetLevel(l)
}
if cfg.VersionAndExit {
fmt.Printf("nginx-sso %s\n", version)
os.Exit(0)
}
// Set sane defaults for main configuration
mainCfg.Cookie.Prefix = "nginx-sso"
mainCfg.Cookie.Expire = 3600
mainCfg.Listen.Addr = "127.0.0.1"
mainCfg.Listen.Port = 8082
mainCfg.AuditLog.TrustedIPHeaders = []string{"X-Forwarded-For", "RemoteAddr", "X-Real-IP"}
mainCfg.AuditLog.Headers = []string{"x-origin-uri"}
}
func loadConfiguration() error {
yamlSource, err := ioutil.ReadFile(cfg.ConfigFile)
if err != nil {
return fmt.Errorf("Unable to read configuration file: %s", err)
}
if err = yaml.Unmarshal(yamlSource, &mainCfg); err != nil {
return fmt.Errorf("Unable to load configuration file: %s", err)
}
if mainCfg.Plugins.Directory != "" {
if err = loadPlugins(mainCfg.Plugins.Directory); err != nil {
return errors.Wrap(err, "Unable to load plugins")
}
}
if err = initializeAuthenticators(yamlSource); err != nil {
return fmt.Errorf("Unable to configure authentication: %s", err)
}
if err = initializeMFAProviders(yamlSource); err != nil {
log.WithError(err).Fatal("Unable to configure MFA providers")
}
return nil
}
func main() {
if err := loadConfiguration(); err != nil {
log.WithError(err).Fatal("Unable to load configuration")
}
cookieStore = sessions.NewCookieStore([]byte(mainCfg.Cookie.AuthKey))
http.HandleFunc("/auth", handleAuthRequest)
http.HandleFunc("/login", handleLoginRequest)
http.HandleFunc("/logout", handleLogoutRequest)
go http.ListenAndServe(
fmt.Sprintf("%s:%d", mainCfg.Listen.Addr, mainCfg.Listen.Port),
context.ClearHandler(http.DefaultServeMux),
)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGHUP)
for sig := range sigChan {
switch sig {
case syscall.SIGHUP:
if err := loadConfiguration(); err != nil {
log.WithError(err).Error("Unable to reload configuration")
}
default:
log.Fatalf("Received unexpected signal: %v", sig)
}
}
}
func handleAuthRequest(res http.ResponseWriter, r *http.Request) {
user, groups, err := detectUser(res, r)
switch err {
case plugins.ErrNoValidUserFound:
mainCfg.AuditLog.Log(auditEventValidate, r, map[string]string{"result": "no valid user found"}) // #nosec G104 - This is only logging
http.Error(res, "No valid user found", http.StatusUnauthorized)
case nil:
if !mainCfg.ACL.HasAccess(user, groups, r) {
mainCfg.AuditLog.Log(auditEventAccessDenied, r, map[string]string{"username": user}) // #nosec G104 - This is only logging
http.Error(res, "Access denied for this resource", http.StatusForbidden)
return
}
mainCfg.AuditLog.Log(auditEventValidate, r, map[string]string{"result": "valid user found", "username": user}) // #nosec G104 - This is only logging
res.Header().Set("X-Username", user)
res.WriteHeader(http.StatusOK)
default:
log.WithError(err).Error("Error while handling auth request")
http.Error(res, "Something went wrong", http.StatusInternalServerError)
}
}
func handleLoginRequest(res http.ResponseWriter, r *http.Request) {
redirURL, err := getRedirectURL(r)
if err != nil {
http.Error(res, "Invalid redirect URL specified", http.StatusBadRequest)
}
if _, _, err := detectUser(res, r); err == nil {
// There is already a valid user
http.Redirect(res, r, redirURL, http.StatusFound)
return
}
auditFields := map[string]string{
"go": redirURL,
}
if r.Method == "POST" {
// Simple authentication
user, mfaCfgs, err := loginUser(res, r)
switch err {
case plugins.ErrNoValidUserFound:
auditFields["reason"] = "invalid credentials"
mainCfg.AuditLog.Log(auditEventLoginFailure, r, auditFields) // #nosec G104 - This is only logging
http.Redirect(res, r, "/login?go="+url.QueryEscape(redirURL), http.StatusFound)
return
case nil:
// Don't handle for now, MFA validation comes first
default:
auditFields["reason"] = "error"
auditFields["error"] = err.Error()
mainCfg.AuditLog.Log(auditEventLoginFailure, r, auditFields) // #nosec G104 - This is only logging
log.WithError(err).Error("Login failed with unexpected error")
http.Redirect(res, r, "/login?go="+url.QueryEscape(redirURL), http.StatusFound)
return
}
// MFA validation against configs from login
err = validateMFA(res, r, user, mfaCfgs)
switch err {
case plugins.ErrNoValidUserFound:
auditFields["reason"] = "invalid credentials"
mainCfg.AuditLog.Log(auditEventLoginFailure, r, auditFields) // #nosec G104 - This is only logging
res.Header().Del("Set-Cookie") // Remove login cookie
http.Redirect(res, r, "/login?go="+url.QueryEscape(redirURL), http.StatusFound)
return
case nil:
mainCfg.AuditLog.Log(auditEventLoginSuccess, r, auditFields) // #nosec G104 - This is only logging
http.Redirect(res, r, redirURL, http.StatusFound)
return
default:
auditFields["reason"] = "error"
auditFields["error"] = err.Error()
mainCfg.AuditLog.Log(auditEventLoginFailure, r, auditFields) // #nosec G104 - This is only logging
log.WithError(err).Error("Login failed with unexpected error")
res.Header().Del("Set-Cookie") // Remove login cookie
http.Redirect(res, r, "/login?go="+url.QueryEscape(redirURL), http.StatusFound)
return
}
}
tpl := pongo2.Must(pongo2.FromFile(path.Join(cfg.TemplateDir, "index.html")))
if err := tpl.ExecuteWriter(pongo2.Context{
"active_methods": getFrontendAuthenticators(),
"go": redirURL,
"login": mainCfg.Login,
}, res); err != nil {
log.WithError(err).Error("Unable to render template")
http.Error(res, "Something went wrong", http.StatusInternalServerError)
}
}
func handleLogoutRequest(res http.ResponseWriter, r *http.Request) {
redirURL, err := getRedirectURL(r)
if err != nil {
http.Error(res, "Invalid redirect URL specified", http.StatusBadRequest)
}
mainCfg.AuditLog.Log(auditEventLogout, r, nil) // #nosec G104 - This is only logging
if err := logoutUser(res, r); err != nil {
log.WithError(err).Error("Failed to logout user")
http.Error(res, "Something went wrong", http.StatusInternalServerError)
return
}
http.Redirect(res, r, redirURL, http.StatusFound)
}