2018-06-26 12:28:47 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2019-01-22 12:40:14 +00:00
|
|
|
"encoding/json"
|
2019-01-22 10:50:55 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/namsral/flag"
|
2019-01-22 12:40:14 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2018-06-26 12:28:47 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
// Vars
|
2019-01-22 10:50:55 +00:00
|
|
|
var fw *ForwardAuth
|
2019-01-22 12:40:14 +00:00
|
|
|
var log logrus.FieldLogger
|
2018-06-26 12:28:47 +01:00
|
|
|
|
|
|
|
// Primary handler
|
|
|
|
func handler(w http.ResponseWriter, r *http.Request) {
|
2019-01-22 12:58:24 +00:00
|
|
|
// Logging setup
|
|
|
|
logger := log.WithFields(logrus.Fields{
|
|
|
|
"RemoteAddr": r.RemoteAddr,
|
|
|
|
})
|
|
|
|
logger.WithFields(logrus.Fields{
|
|
|
|
"Headers": r.Header,
|
|
|
|
}).Debugf("Handling request")
|
2019-01-22 12:40:14 +00:00
|
|
|
|
2019-01-22 10:50:55 +00:00
|
|
|
// Parse uri
|
|
|
|
uri, err := url.Parse(r.Header.Get("X-Forwarded-Uri"))
|
|
|
|
if err != nil {
|
2019-01-22 12:40:14 +00:00
|
|
|
logger.Errorf("Error parsing X-Forwarded-Uri, %v", err)
|
2019-01-22 10:50:55 +00:00
|
|
|
http.Error(w, "Service unavailable", 503)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle callback
|
|
|
|
if uri.Path == fw.Path {
|
2019-01-22 12:40:14 +00:00
|
|
|
logger.Debugf("Passing request to auth callback")
|
|
|
|
handleCallback(w, r, uri.Query(), logger)
|
2019-01-22 10:50:55 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get auth cookie
|
|
|
|
c, err := r.Cookie(fw.CookieName)
|
|
|
|
if err != nil {
|
|
|
|
// Error indicates no cookie, generate nonce
|
|
|
|
err, nonce := fw.Nonce()
|
|
|
|
if err != nil {
|
2019-01-22 12:40:14 +00:00
|
|
|
logger.Errorf("Error generating nonce, %v", err)
|
2019-01-22 10:50:55 +00:00
|
|
|
http.Error(w, "Service unavailable", 503)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the CSRF cookie
|
|
|
|
http.SetCookie(w, fw.MakeCSRFCookie(r, nonce))
|
2019-01-22 12:40:14 +00:00
|
|
|
logger.Debug("Set CSRF cookie and redirecting to google login")
|
2019-01-22 10:50:55 +00:00
|
|
|
|
|
|
|
// Forward them on
|
|
|
|
http.Redirect(w, r, fw.GetLoginURL(r, nonce), http.StatusTemporaryRedirect)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate cookie
|
|
|
|
valid, email, err := fw.ValidateCookie(r, c)
|
|
|
|
if !valid {
|
2019-01-22 12:40:14 +00:00
|
|
|
logger.Errorf("Invalid cookie: %v", err)
|
2019-01-22 10:50:55 +00:00
|
|
|
http.Error(w, "Not authorized", 401)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate user
|
|
|
|
valid = fw.ValidateEmail(email)
|
|
|
|
if !valid {
|
2019-01-22 12:40:14 +00:00
|
|
|
logger.WithFields(logrus.Fields{
|
|
|
|
"email": email,
|
|
|
|
}).Errorf("Invalid email")
|
2019-01-22 10:50:55 +00:00
|
|
|
http.Error(w, "Not authorized", 401)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Valid request
|
2019-01-22 12:40:14 +00:00
|
|
|
logger.Debugf("Allowing valid request ")
|
2019-01-22 10:50:55 +00:00
|
|
|
w.Header().Set("X-Forwarded-User", email)
|
|
|
|
w.WriteHeader(200)
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Authenticate user after they have come back from google
|
2019-01-22 12:40:14 +00:00
|
|
|
func handleCallback(w http.ResponseWriter, r *http.Request, qs url.Values,
|
|
|
|
logger logrus.FieldLogger) {
|
2019-01-22 10:50:55 +00:00
|
|
|
// Check for CSRF cookie
|
|
|
|
csrfCookie, err := r.Cookie(fw.CSRFCookieName)
|
|
|
|
if err != nil {
|
2019-01-22 12:40:14 +00:00
|
|
|
logger.Warn("Missing csrf cookie")
|
2019-01-22 10:50:55 +00:00
|
|
|
http.Error(w, "Not authorized", 401)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate state
|
|
|
|
state := qs.Get("state")
|
|
|
|
valid, redirect, err := fw.ValidateCSRFCookie(csrfCookie, state)
|
|
|
|
if !valid {
|
2019-01-22 12:40:14 +00:00
|
|
|
logger.WithFields(logrus.Fields{
|
|
|
|
"csrf": csrfCookie.Value,
|
|
|
|
"state": state,
|
2019-01-22 13:10:03 +00:00
|
|
|
}).Warnf("Error validating csrf cookie: %v", err)
|
2019-01-22 10:50:55 +00:00
|
|
|
http.Error(w, "Not authorized", 401)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clear CSRF cookie
|
|
|
|
http.SetCookie(w, fw.ClearCSRFCookie(r))
|
|
|
|
|
|
|
|
// Exchange code for token
|
|
|
|
token, err := fw.ExchangeCode(r, qs.Get("code"))
|
|
|
|
if err != nil {
|
2019-01-22 12:40:14 +00:00
|
|
|
logger.Errorf("Code exchange failed with: %v", err)
|
2019-01-22 10:50:55 +00:00
|
|
|
http.Error(w, "Service unavailable", 503)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get user
|
|
|
|
user, err := fw.GetUser(token)
|
|
|
|
if err != nil {
|
2019-01-22 12:40:14 +00:00
|
|
|
logger.Errorf("Error getting user: %s", err)
|
2019-01-22 10:50:55 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate cookie
|
|
|
|
http.SetCookie(w, fw.MakeCookie(r, user.Email))
|
2019-01-22 12:40:14 +00:00
|
|
|
logger.WithFields(logrus.Fields{
|
|
|
|
"user": user.Email,
|
|
|
|
}).Infof("Generated auth cookie")
|
2019-01-22 10:50:55 +00:00
|
|
|
|
|
|
|
// Redirect
|
|
|
|
http.Redirect(w, r, redirect, http.StatusTemporaryRedirect)
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Main
|
|
|
|
func main() {
|
2019-01-22 10:50:55 +00:00
|
|
|
// Parse options
|
|
|
|
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", "", "Deprecated")
|
|
|
|
cookieSecure := flag.Bool("cookie-secure", true, "Use secure cookies")
|
|
|
|
domainList := flag.String("domain", "", "Comma separated list of email domains to allow")
|
|
|
|
emailWhitelist := flag.String("whitelist", "", "Comma separated list of emails to allow")
|
|
|
|
prompt := flag.String("prompt", "", "Space separated list of OpenID prompt options")
|
2019-01-22 12:40:14 +00:00
|
|
|
logLevel := flag.String("log-level", "warn", "Log level: trace, debug, info, warn, error, fatal, panic")
|
|
|
|
logFormat := flag.String("log-format", "text", "Log format: text, json, pretty")
|
2019-01-22 10:50:55 +00:00
|
|
|
|
|
|
|
flag.Parse()
|
|
|
|
|
2019-01-22 12:40:14 +00:00
|
|
|
// Setup logger
|
|
|
|
log = CreateLogger(*logLevel, *logFormat)
|
|
|
|
|
2019-01-22 13:10:03 +00:00
|
|
|
// Backwards compatibility
|
2019-01-22 10:50:55 +00:00
|
|
|
if *secret == "" && *cookieSecret != "" {
|
|
|
|
*secret = *cookieSecret
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for show stopper errors
|
2019-01-22 12:40:14 +00:00
|
|
|
if *clientId == "" || *clientSecret == "" || *secret == "" {
|
|
|
|
log.Fatal("client-id, client-secret and secret must all be set")
|
2019-01-22 10:50:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Parse lists
|
|
|
|
var cookieDomains []CookieDomain
|
|
|
|
if *cookieDomainList != "" {
|
|
|
|
for _, d := range strings.Split(*cookieDomainList, ",") {
|
|
|
|
cookieDomain := NewCookieDomain(d)
|
|
|
|
cookieDomains = append(cookieDomains, *cookieDomain)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var domain []string
|
|
|
|
if *domainList != "" {
|
|
|
|
domain = strings.Split(*domainList, ",")
|
|
|
|
}
|
|
|
|
var whitelist []string
|
|
|
|
if *emailWhitelist != "" {
|
|
|
|
whitelist = strings.Split(*emailWhitelist, ",")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Setup
|
|
|
|
fw = &ForwardAuth{
|
|
|
|
Path: fmt.Sprintf("/%s", *path),
|
|
|
|
Lifetime: time.Second * time.Duration(*lifetime),
|
|
|
|
Secret: []byte(*secret),
|
|
|
|
AuthHost: *authHost,
|
|
|
|
|
|
|
|
ClientId: *clientId,
|
|
|
|
ClientSecret: *clientSecret,
|
|
|
|
Scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
|
|
|
|
LoginURL: &url.URL{
|
|
|
|
Scheme: "https",
|
|
|
|
Host: "accounts.google.com",
|
|
|
|
Path: "/o/oauth2/auth",
|
|
|
|
},
|
|
|
|
TokenURL: &url.URL{
|
|
|
|
Scheme: "https",
|
|
|
|
Host: "www.googleapis.com",
|
|
|
|
Path: "/oauth2/v3/token",
|
|
|
|
},
|
|
|
|
UserURL: &url.URL{
|
|
|
|
Scheme: "https",
|
|
|
|
Host: "www.googleapis.com",
|
|
|
|
Path: "/oauth2/v2/userinfo",
|
|
|
|
},
|
|
|
|
|
|
|
|
CookieName: *cookieName,
|
|
|
|
CSRFCookieName: *cSRFCookieName,
|
|
|
|
CookieDomains: cookieDomains,
|
|
|
|
CookieSecure: *cookieSecure,
|
|
|
|
|
|
|
|
Domain: domain,
|
|
|
|
Whitelist: whitelist,
|
|
|
|
|
|
|
|
Prompt: *prompt,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Attach handler
|
|
|
|
http.HandleFunc("/", handler)
|
|
|
|
|
2019-01-22 12:40:14 +00:00
|
|
|
// Start
|
|
|
|
jsonConf, _ := json.Marshal(fw)
|
|
|
|
log.Debugf("Starting with options: %s", string(jsonConf))
|
|
|
|
log.Info("Listening on :4181")
|
|
|
|
log.Info(http.ListenAndServe(":4181", nil))
|
2018-06-26 12:28:47 +01:00
|
|
|
}
|