Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: Enable SSO by listning for http header: REMOTE_USER #632

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions handler/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import (
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
"github.com/ngoduykhanh/wireguard-ui/model"
"github.com/ngoduykhanh/wireguard-ui/store/jsondb"
"github.com/ngoduykhanh/wireguard-ui/util"
"github.com/rs/xid"
)

func ValidSession(next echo.HandlerFunc) echo.HandlerFunc {
Expand Down Expand Up @@ -43,6 +47,86 @@ func NeedsAdmin(next echo.HandlerFunc) echo.HandlerFunc {
}
}

// SSOauth uses external authentication (usually by reverseproxy) in the form of HTTP header REMOTE_USER
func SSOauth(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if !util.RemoteUser {
return next(c)
}
if !isValidSession(c) {
remoteUser := c.Request().Header.Get("REMOTE_USER")
if remoteUser == "" {
// TODO: Better error handling
log.Infof("No REMOTE_USER in reqest. Bailing out.")
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/")
}
log.Debugf("No valid session for REMOTE_USER: %s", remoteUser)

db := c.Get("db").(*jsondb.JsonDB)
dbuser, err := db.GetUserByName(remoteUser)
if err != nil {
log.Infof("User %s not in database, creating user", remoteUser)
newUser := model.User{
Username: remoteUser,
Admin: false,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to add a flag where you can specify the admin username.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a SSO context this would preferably be done on the IdP and provisioning entitlements so the app dont have to do it. That also works well when a user is no longer a admin.

The provided code is more a proof of concept that works in our setup, but I did have to do an bootstrap by starting the applikation and edit me to admin and then switching on SSO.

For a real use I think it would be better to listen to some value from the IdP than a set list of users that should be admins.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, there is no protocol for transferring rights or anything else. Even remote-user is not something official, but just one of two popular options. Second is X-Forwarded-User.
I have my own authorizing proxy and I have tried many services. Usually, you just specify the admin for applications. This is a common approach.

}
err = db.SaveUser(newUser)
if err != nil {
// TODO: Better error handling
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/")
}
// Update dbuser from database
dbuser, err = db.GetUserByName(remoteUser)
if err != nil {
// TODO: Better error handling
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/")
}

} else {
log.Debugf("Got user from db: %s", dbuser.Username)
}

// Set session for REMOTE_USER
ageMax := 0

cookiePath := util.GetCookiePath()

sess, _ := session.Get("session", c)
sess.Options = &sessions.Options{
Path: cookiePath,
MaxAge: ageMax,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}

// set session_token
tokenUID := xid.New().String()
now := time.Now().UTC().Unix()
sess.Values["username"] = dbuser.Username
sess.Values["user_hash"] = util.GetDBUserCRC32(dbuser)
sess.Values["admin"] = dbuser.Admin
sess.Values["session_token"] = tokenUID
sess.Values["max_age"] = ageMax
sess.Values["created_at"] = now
sess.Values["updated_at"] = now
sess.Save(c.Request(), c.Response())

// set session_token in cookie
cookie := new(http.Cookie)
cookie.Name = "session_token"
cookie.Path = cookiePath
cookie.Value = tokenUID
cookie.MaxAge = ageMax
cookie.HttpOnly = true
cookie.SameSite = http.SameSiteLaxMode
c.SetCookie(cookie)

return c.Redirect(http.StatusTemporaryRedirect, util.BasePath)
}
return next(c)
}
}

func isValidSession(c echo.Context) bool {
if util.DisableLogin {
return true
Expand Down
8 changes: 6 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var (
buildTime = fmt.Sprintf(time.Now().UTC().Format("01-02-2006 15:04:05"))
// configuration variables
flagDisableLogin = false
flagRemoteUser = false
flagBindAddress = "0.0.0.0:5000"
flagSmtpHostname = "127.0.0.1"
flagSmtpPort = 25
Expand Down Expand Up @@ -77,6 +78,7 @@ var embeddedAssets embed.FS
func init() {
// command-line flags and env variables
flag.BoolVar(&flagDisableLogin, "disable-login", util.LookupEnvOrBool("DISABLE_LOGIN", flagDisableLogin), "Disable authentication on the app. This is potentially dangerous.")
flag.BoolVar(&flagRemoteUser, "remote_user", util.LookupEnvOrBool("REMOTE_USER", flagRemoteUser), "Use HTTP header REMOTE_USER for auth. Commonly used with SSO and a proxy funcion.")
flag.StringVar(&flagBindAddress, "bind-address", util.LookupEnvOrString("BIND_ADDRESS", flagBindAddress), "Address:Port to which the app will be bound.")
flag.StringVar(&flagSmtpHostname, "smtp-hostname", util.LookupEnvOrString("SMTP_HOSTNAME", flagSmtpHostname), "SMTP Hostname")
flag.IntVar(&flagSmtpPort, "smtp-port", util.LookupEnvOrInt("SMTP_PORT", flagSmtpPort), "SMTP Port")
Expand Down Expand Up @@ -126,6 +128,7 @@ func init() {

// update runtime config
util.DisableLogin = flagDisableLogin
util.RemoteUser = flagRemoteUser
util.BindAddress = flagBindAddress
util.SmtpHostname = flagSmtpHostname
util.SmtpPort = flagSmtpPort
Expand Down Expand Up @@ -161,6 +164,7 @@ func init() {
fmt.Println("Build Time\t:", buildTime)
fmt.Println("Git Repo\t:", "https://github.com/ngoduykhanh/wireguard-ui")
fmt.Println("Authentication\t:", !util.DisableLogin)
fmt.Println("Remote_user\t:", util.RemoteUser)
fmt.Println("Bind address\t:", util.BindAddress)
//fmt.Println("Sendgrid key\t:", util.SendgridApiKey)
fmt.Println("Email from\t:", util.EmailFrom)
Expand Down Expand Up @@ -206,9 +210,9 @@ func main() {
}

// register routes
app := router.New(tmplDir, extraData, util.SessionSecret)
app := router.New(tmplDir, extraData, util.SessionSecret, db)

app.GET(util.BasePath, handler.WireGuardClients(db), handler.ValidSession, handler.RefreshSession)
app.GET(util.BasePath, handler.WireGuardClients(db), handler.SSOauth, handler.ValidSession, handler.RefreshSession)

// Important: Make sure that all non-GET routes check the request content type using handler.ContentTypeJson to
// mitigate CSRF attacks. This is effective, because browsers don't allow setting the Content-Type header on
Expand Down
11 changes: 10 additions & 1 deletion router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log"
"github.com/ngoduykhanh/wireguard-ui/store/jsondb"
"github.com/ngoduykhanh/wireguard-ui/util"
)

Expand Down Expand Up @@ -48,7 +49,7 @@ func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c
}

// New function
func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo.Echo {
func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte, db *jsondb.JsonDB) *echo.Echo {
e := echo.New()

cookiePath := util.GetCookiePath()
Expand All @@ -60,6 +61,14 @@ func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo

e.Use(session.Middleware(cookieStore))

// Add db to context so middlewares can use it.
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Set("db", db)
return next(c)
}
})

// read html template file to string
tmplBaseString, err := util.StringFromEmbedFile(tmplDir, "base.html")
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions util/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
// Runtime config
var (
DisableLogin bool
RemoteUser bool
BindAddress string
SmtpHostname string
SmtpPort int
Expand Down