Add config file support

This commit is contained in:
Jehiah Czebotar 2014-11-09 14:51:10 -05:00
parent 899749a08d
commit d4fe9a4f57
8 changed files with 243 additions and 118 deletions

View File

@ -2,6 +2,8 @@ language: go
install:
- go get github.com/bmizerany/assert
- go get github.com/bitly/go-simplejson
- go get github.com/mreiferson/go-options
- go get github.com/BurntSushi/toml
notifications:
email: false

View File

@ -42,28 +42,40 @@ intend to run `google_auth_proxy` on.
5. Take note of the **Client ID** and **Client Secret**
## Command Line Options
## Configuration
`google_auth_proxy` can be configured via [config file](#config-file), [command line options](#command-line-options) or [environment variables](#environment-variables).
### Config File
An example [google_auth_proxy.cfg](contrib/google_auth_proxy.cfg.example) config file is in the contrib directory. It can be used by specifying `-config=/etc/google_auth_proxy.cfg`
### Command Line Options
```
Usage of ./google_auth_proxy:
Usage of google_auth_proxy:
-authenticated-emails-file="": authenticate against emails via file (one per line)
-client-id="": the Google OAuth Client ID: ie: "123456.apps.googleusercontent.com"
-client-secret="": the OAuth Client Secret
-cookie-domain="": an optional cookie domain to force cookies to
-cookie-expire=168h: expire timeframe for cookie
-config="": path to config file
-cookie-domain="": an optional cookie domain to force cookies to (ie: .yourcompany.com)
-cookie-expire=168h0m0s: expire timeframe for cookie
-cookie-https-only=false: set HTTPS only cookie
-cookie-secret="": the seed string for secure cookies
-google-apps-domain=[]: authenticate against the given google apps domain (may be given multiple times)
-google-apps-domain=: authenticate against the given Google apps domain (may be given multiple times)
-htpasswd-file="": additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption
-http-address="127.0.0.1:4180": <addr>:<port> to listen on for HTTP clients
-pass-basic-auth=true: pass HTTP Basic Auth information to upstream
-pass-basic-auth=true: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
-redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
-upstream=[]: the http url(s) of the upstream endpoint. If multiple, routing is based on path
-upstream=: the http url(s) of the upstream endpoint. If multiple, routing is based on path
-version=false: print version string
```
### Environment variables
## Example Configuration
The environment variables `google_auth_client_id`, `google_auth_secret` and `google_auth_cookie_secret` can be used in place of the corresponding command-line arguments.
### Example Nginx Configuration
This example has a [Nginx](http://nginx.org/) SSL endpoint proxying to `google_auth_proxy` on port `4180`.
`google_auth_proxy` then authenticates requests for an upstream application running on port `8080`. The external
@ -105,13 +117,10 @@ The command line to run `google_auth_proxy` would look like this:
--client-secret=...
```
## Environment variables
The environment variables `google_auth_client_id`, `google_auth_secret` and `google_auth_cookie_secret` can be used in place of the corresponding command-line arguments.
## Endpoint Documentation
Google Auth Proxy responds directly to the following endpoints. All other endpoints will be authenticated.
Google Auth Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated.
* /ping - returns an 200 OK response
* /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)

View File

@ -0,0 +1,44 @@
## Google Auth Proxy Config File
## https://github.com/bitly/google_auth_proxy
## <addr>:<port> to listen on for HTTP clients
# http_address = "127.0.0.1:4180"
## the OAuth Redirect URL.
# redirect_url = "https://internalapp.yourcompany.com/oauth2/callback"
## the http url(s) of the upstream endpoint. If multiple, routing is based on path
# upstreams = [
# "http://127.0.0.1:8080/"
# ]
## pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
# pass_basic_auth = true
## Google Apps Domains to allow authentication for
# google_apps_domains = [
# "yourcompany.com"
# ]
## The Google OAuth Client ID, Secret
# client_id = "123456.apps.googleusercontent.com"
# client_secret = ""
## Authenticated Email Addresses File (one email per line)
# authenticated_emails_file = ""
## Htpasswd File (optional)
## Additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption
## enabling exposes a username/login signin form
# htpasswd_file = ""
## Cookie Settings
## Secret - the seed string for secure cookies
## Domain - optional cookie domain to force cookies to (ie: .yourcompany.com)
## Expire - expire timeframe for cookie
# cookie_secret = ""
# cookie_domain = ""
# cookie_expire = "168h"
# cookie_https_only = false

136
main.go
View File

@ -6,49 +6,63 @@ import (
"log"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/BurntSushi/toml"
"github.com/mreiferson/go-options"
)
const VERSION = "0.1.0"
var (
showVersion = flag.Bool("version", false, "print version string")
httpAddr = flag.String("http-address", "127.0.0.1:4180", "<addr>:<port> to listen on for HTTP clients")
redirectUrl = flag.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
clientID = flag.String("client-id", "", "the Google OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
clientSecret = flag.String("client-secret", "", "the OAuth Client Secret")
passBasicAuth = flag.Bool("pass-basic-auth", true, "pass HTTP Basic Auth information to upstream")
htpasswdFile = flag.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption")
cookieSecret = flag.String("cookie-secret", "", "the seed string for secure cookies")
cookieDomain = flag.String("cookie-domain", "", "an optional cookie domain to force cookies to")
cookieExpire = flag.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie")
cookieHttpsOnly = flag.Bool("cookie-https-only", false, "set HTTPS only cookie")
authenticatedEmailsFile = flag.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
googleAppsDomains = StringArray{}
upstreams = StringArray{}
)
func init() {
flag.Var(&googleAppsDomains, "google-apps-domain", "authenticate against the given google apps domain (may be given multiple times)")
flag.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint. If multiple, routing is based on path")
}
func main() {
flagSet := flag.NewFlagSet("google_auth_proxy", flag.ExitOnError)
flag.Parse()
googleAppsDomains := StringArray{}
upstreams := StringArray{}
config := flagSet.String("config", "", "path to config file")
showVersion := flagSet.Bool("version", false, "print version string")
flagSet.String("http-address", "127.0.0.1:4180", "<addr>:<port> to listen on for HTTP clients")
flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
flagSet.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint. If multiple, routing is based on path")
flagSet.Bool("pass-basic-auth", true, "pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream")
flagSet.Var(&googleAppsDomains, "google-apps-domain", "authenticate against the given Google apps domain (may be given multiple times)")
flagSet.String("client-id", "", "the Google OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
flagSet.String("client-secret", "", "the OAuth Client Secret")
flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption")
flagSet.String("cookie-secret", "", "the seed string for secure cookies")
flagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)")
flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie")
flagSet.Bool("cookie-https-only", false, "set HTTPS only cookie")
flagSet.Parse(os.Args[1:])
opts := NewOptions()
var cfg map[string]interface{}
if *config != "" {
_, err := toml.DecodeFile(*config, &cfg)
if err != nil {
log.Fatalf("ERROR: failed to load config file %s - %s", *config, err)
}
}
options.Resolve(opts, flagSet, cfg)
// Try to use env for secrets if no flag is set
if *clientID == "" {
*clientID = os.Getenv("google_auth_client_id")
// TODO: better parsing of these values
if opts.ClientID == "" {
opts.ClientID = os.Getenv("google_auth_client_id")
}
if *clientSecret == "" {
*clientSecret = os.Getenv("google_auth_secret")
if opts.ClientSecret == "" {
opts.ClientSecret = os.Getenv("google_auth_secret")
}
if *cookieSecret == "" {
*cookieSecret = os.Getenv("google_auth_cookie_secret")
if opts.CookieSecret == "" {
opts.CookieSecret = os.Getenv("google_auth_cookie_secret")
}
if *showVersion {
@ -56,59 +70,41 @@ func main() {
return
}
if len(upstreams) < 1 {
log.Fatal("missing --upstream")
}
if *cookieSecret == "" {
log.Fatal("missing --cookie-secret")
}
if *clientID == "" {
log.Fatal("missing --client-id")
}
if *clientSecret == "" {
log.Fatal("missing --client-secret")
}
var upstreamUrls []*url.URL
for _, u := range upstreams {
upstreamUrl, err := url.Parse(u)
if err != nil {
log.Fatalf("error parsing --upstream %s", err.Error())
}
upstreamUrls = append(upstreamUrls, upstreamUrl)
}
redirectUrl, err := url.Parse(*redirectUrl)
err := opts.Validate()
if err != nil {
log.Fatalf("error parsing --redirect-url %s", err.Error())
log.Printf("%s", err)
os.Exit(1)
}
validator := NewValidator(googleAppsDomains, *authenticatedEmailsFile)
oauthproxy := NewOauthProxy(upstreamUrls, *clientID, *clientSecret, validator)
oauthproxy.SetRedirectUrl(redirectUrl)
if len(googleAppsDomains) != 0 && *authenticatedEmailsFile == "" {
if len(googleAppsDomains) > 1 {
oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(googleAppsDomains, ", "))
validator := NewValidator(opts.GoogleAppsDomains, opts.AuthenticatedEmailsFile)
oauthproxy := NewOauthProxy(opts, validator)
if len(opts.GoogleAppsDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
if len(opts.GoogleAppsDomains) > 1 {
oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.GoogleAppsDomains, ", "))
} else {
oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using %v", googleAppsDomains[0])
oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using %v", opts.GoogleAppsDomains[0])
}
}
if *htpasswdFile != "" {
oauthproxy.HtpasswdFile, err = NewHtpasswdFromFile(*htpasswdFile)
if opts.HtpasswdFile != "" {
oauthproxy.HtpasswdFile, err = NewHtpasswdFromFile(opts.HtpasswdFile)
if err != nil {
log.Fatalf("FATAL: unable to open %s %s", *htpasswdFile, err.Error())
log.Fatalf("FATAL: unable to open %s %s", opts.HtpasswdFile, err)
}
}
listener, err := net.Listen("tcp", *httpAddr)
listener, err := net.Listen("tcp", opts.HttpAddress)
if err != nil {
log.Fatalf("FATAL: listen (%s) failed - %s", *httpAddr, err.Error())
log.Fatalf("FATAL: listen (%s) failed - %s", opts.HttpAddress, err)
}
log.Printf("listening on %s", *httpAddr)
log.Printf("listening on %s", opts.HttpAddress)
server := &http.Server{Handler: oauthproxy}
err = server.Serve(listener)
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
log.Printf("ERROR: http.Serve() - %s", err.Error())
log.Printf("ERROR: http.Serve() - %s", err)
}
log.Printf("HTTP: closing %s", listener.Addr().String())
log.Printf("HTTP: closing %s", listener.Addr())
}

View File

@ -22,9 +22,12 @@ const oauthStartPath = "/oauth2/start"
const oauthCallbackPath = "/oauth2/callback"
type OauthProxy struct {
CookieSeed string
CookieKey string
Validator func(string) bool
CookieSeed string
CookieKey string
CookieDomain string
CookieHttpsOnly bool
CookieExpire time.Duration
Validator func(string) bool
redirectUrl *url.URL // the url to receive requests at
oauthRedemptionUrl *url.URL // endpoint to redeem the code
@ -35,40 +38,41 @@ type OauthProxy struct {
SignInMessage string
HtpasswdFile *HtpasswdFile
serveMux *http.ServeMux
PassBasicAuth bool
}
func NewOauthProxy(proxyUrls []*url.URL, clientID string, clientSecret string, validator func(string) bool) *OauthProxy {
func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
login, _ := url.Parse("https://accounts.google.com/o/oauth2/auth")
redeem, _ := url.Parse("https://accounts.google.com/o/oauth2/token")
serveMux := http.NewServeMux()
for _, u := range proxyUrls {
for _, u := range opts.proxyUrls {
path := u.Path
if len(path) == 0 {
path = "/"
}
u.Path = ""
log.Printf("mapping %s => %s", path, u)
log.Printf("mapping path %q => upstream %q", path, u)
serveMux.Handle(path, httputil.NewSingleHostReverseProxy(u))
}
return &OauthProxy{
CookieKey: "_oauthproxy",
CookieSeed: *cookieSecret,
Validator: validator,
redirectUrl := opts.redirectUrl
redirectUrl.Path = oauthCallbackPath
clientID: clientID,
clientSecret: clientSecret,
return &OauthProxy{
CookieKey: "_oauthproxy",
CookieSeed: opts.CookieSecret,
CookieDomain: opts.CookieDomain,
CookieHttpsOnly: opts.CookieHttpsOnly,
CookieExpire: opts.CookieExpire,
Validator: validator,
clientID: opts.ClientID,
clientSecret: opts.ClientSecret,
oauthScope: "profile email",
oauthRedemptionUrl: redeem,
oauthLoginUrl: login,
serveMux: serveMux,
redirectUrl: redirectUrl,
PassBasicAuth: opts.PassBasicAuth,
}
}
func (p *OauthProxy) SetRedirectUrl(redirectUrl *url.URL) {
redirectUrl.Path = oauthCallbackPath
p.redirectUrl = redirectUrl
}
func (p *OauthProxy) GetLoginURL(redirectUrl string) string {
params := url.Values{}
params.Add("redirect_uri", p.redirectUrl.String())
@ -164,8 +168,8 @@ func jwtDecodeSegment(seg string) ([]byte, error) {
func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
domain := strings.Split(req.Host, ":")[0]
if *cookieDomain != "" && strings.HasSuffix(domain, *cookieDomain) {
domain = *cookieDomain
if p.CookieDomain != "" && strings.HasSuffix(domain, p.CookieDomain) {
domain = p.CookieDomain
}
cookie := &http.Cookie{
Name: p.CookieKey,
@ -181,8 +185,8 @@ func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val string) {
domain := strings.Split(req.Host, ":")[0] // strip the port (if any)
if *cookieDomain != "" && strings.HasSuffix(domain, *cookieDomain) {
domain = *cookieDomain
if p.CookieDomain != "" && strings.HasSuffix(domain, p.CookieDomain) {
domain = p.CookieDomain
}
cookie := &http.Cookie{
Name: p.CookieKey,
@ -190,8 +194,8 @@ func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val st
Path: "/",
Domain: domain,
HttpOnly: true,
Secure: *cookieHttpsOnly,
Expires: time.Now().Add(*cookieExpire),
Secure: p.CookieHttpsOnly,
Expires: time.Now().Add(p.CookieExpire),
}
http.SetCookie(rw, cookie)
}
@ -267,11 +271,11 @@ func (p *OauthProxy) GetRedirect(req *http.Request) (string, error) {
func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// check if this is a redirect back at the end of oauth
remoteIP := req.Header.Get("X-Real-IP")
if remoteIP == "" {
remoteIP = req.RemoteAddr
remoteAddr := req.RemoteAddr
if req.Header.Get("X-Real-IP") != "" {
remoteAddr += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP"))
}
log.Printf("%s %s %s", remoteIP, req.Method, req.URL.Path)
log.Printf("%s %s %s", remoteAddr, req.Method, req.URL.RequestURI())
var ok bool
var user string
@ -322,7 +326,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
_, email, err := p.redeemCode(req.Form.Get("code"))
if err != nil {
log.Printf("error redeeming code %s", err)
log.Printf("%s error redeeming code %s", remoteAddr, err)
p.ErrorPage(rw, 500, "Internal Error", err.Error())
return
}
@ -334,7 +338,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// set cookie, or deny
if p.Validator(email) {
log.Printf("authenticating %s completed", email)
log.Printf("%s authenticating %s completed", remoteAddr, email)
p.SetCookie(rw, req, email)
http.Redirect(rw, req, redirect, 302)
return
@ -362,13 +366,13 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
if !ok {
log.Printf("invalid cookie")
log.Printf("%s - invalid cookie session", remoteAddr)
p.SignInPage(rw, req, 403)
return
}
// At this point, the user is authenticated. proxy normally
if *passBasicAuth {
if p.PassBasicAuth {
req.SetBasicAuth(user, "")
req.Header["X-Forwarded-User"] = []string{user}
req.Header["X-Forwarded-Email"] = []string{email}

67
options.go Normal file
View File

@ -0,0 +1,67 @@
package main
import (
"errors"
"fmt"
"net/url"
"time"
)
// Configuration Options that can be set by Command Line Flag, or Config File
type Options struct {
HttpAddress string `flag:"http-address" cfg:"http_address"`
RedirectUrl string `flag:"redirect-url" cfg:"redirect_url"`
ClientID string `flag:"client-id" cfg:"client_id"`
ClientSecret string `flag:"client-secret" cfg:"client_secret"`
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"`
CookieSecret string `flag:"cookie-secret" cfg:"cookie_secret"`
CookieDomain string `flag:"cookie-domain" cfg:"cookie_domain"`
CookieExpire time.Duration `flag:"cookie-expire" cfg:"cookie_expire"`
CookieHttpsOnly bool `flag:"cookie-https-only" cfg:"cookie_https_only"`
AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
GoogleAppsDomains []string `flag:"google-apps-domain" cfg:"google_apps_domains"`
Upstreams []string `flag:"upstream" cfg:"upstreams"`
// internal values that are set after config validation
redirectUrl *url.URL
proxyUrls []*url.URL
}
func NewOptions() *Options {
return &Options{}
}
func (o *Options) Validate() error {
if len(o.Upstreams) < 1 {
return errors.New("missing -upstream")
}
if o.CookieSecret == "" {
errors.New("missing -cookie-secret")
}
if o.ClientID == "" {
return errors.New("missing -client-id")
}
if o.ClientSecret == "" {
return errors.New("missing -client-secret")
}
redirectUrl, err := url.Parse(o.RedirectUrl)
if err != nil {
return fmt.Errorf("error parsing -redirect-url=%q %s", o.RedirectUrl, err)
}
o.redirectUrl = redirectUrl
for _, u := range o.Upstreams {
upstreamUrl, err := url.Parse(u)
if err != nil {
return fmt.Errorf("error parsing -upstream=%q %s", upstreamUrl, err)
}
if upstreamUrl.Path == "" {
upstreamUrl.Path = "/"
}
o.proxyUrls = append(o.proxyUrls, upstreamUrl)
}
return nil
}

View File

@ -1,7 +1,7 @@
package main
import (
"fmt"
"strings"
)
type StringArray []string
@ -12,5 +12,5 @@ func (a *StringArray) Set(s string) error {
}
func (a *StringArray) String() string {
return fmt.Sprint(*a)
return strings.Join(*a, ",")
}

3
version.go Normal file
View File

@ -0,0 +1,3 @@
package main
const VERSION = "0.1.0"