From f5b2b20f6784e1af4234144f92a688a95e53cc65 Mon Sep 17 00:00:00 2001 From: Jehiah Czebotar Date: Sun, 7 Jun 2015 21:51:47 -0400 Subject: [PATCH] support TLS directly --- README.md | 52 +++++++++++---- contrib/oauth2_proxy.cfg.example | 7 +- http.go | 106 +++++++++++++++++++++++++++++++ main.go | 38 +++-------- oauthproxy.go | 4 +- options.go | 4 ++ 6 files changed, 167 insertions(+), 44 deletions(-) create mode 100644 http.go diff --git a/README.md b/README.md index 2c2158f..2541a1f 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,18 @@ to validate accounts by email, domain or group. [![Build Status](https://secure.travis-ci.org/bitly/oauth2_proxy.png?branch=master)](http://travis-ci.org/bitly/oauth2_proxy) -![sign_in_page](https://cloud.githubusercontent.com/assets/45028/4970624/7feb7dd8-6886-11e4-93e0-c9904af44ea8.png) +![Sign In Page](https://cloud.githubusercontent.com/assets/45028/4970624/7feb7dd8-6886-11e4-93e0-c9904af44ea8.png) ## Architecture -![oauth2_proxy_arch](https://cloud.githubusercontent.com/assets/45028/7749664/35fef390-ff9d-11e4-8d51-21a7ba78f857.png) +![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png) ## Installation 1. Download [Prebuilt Binary](https://github.com/bitly/oauth2_proxy/releases) (current release is `v1.1.1`) or build with `$ go get github.com/bitly/oauth2_proxy` which will put the binary in `$GOROOT/bin` -2. Register an OAuth Application with a Provider -3. Configure Oauth2 Proxy using config file, command line options, or environment variables -4. Deploy behind a SSL endpoint (example provided for Nginx) +2. Select a Provider and Register an OAuth Application with a Provider +3. Configure OAuth2 Proxy using config file, command line options, or environment variables +4. Configure SSL or Deploy behind a SSL endpoint (example provided for Nginx) ## OAuth Provider Configuration @@ -76,6 +76,10 @@ For LinkedIn, the registration steps are: The [MyUSA](https://alpha.my.usa.gov) authentication service ([GitHub](https://github.com/18F/myusa)) +## Email Authentication + +To authorize by email domain use `--email-domain=yourcompany.com`. To authorize individual email addresses use `--authenticated-emails-file=/path/to/file` with one email per line. To authorize all email addresse use `--email-domain=*`. + ## Configuration `oauth2_proxy` can be configured via [config file](#config-file), [command line options](#command-line-options) or [environment variables](#environment-variables). @@ -107,18 +111,21 @@ Usage of oauth2_proxy: -github-team="": restrict logins to members of this team -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": [http://]: or unix:// to listen on for HTTP clients + -https-address=":443": : to listen on for HTTPS clients -login-url="": Authentication endpoint -pass-access-token=false: pass OAuth access_token to upstream via X-Forwarded-Access-Token header -pass-basic-auth=true: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream -pass-host-header=true: pass the request Host Header to upstream -profile-url="": Profile access endpoint - -provider="": Oauth provider (defaults to Google) + -provider="google": OAuth provider -proxy-prefix="/oauth2": the url root path that this proxy should be nested under (e.g. //sign_in) -redeem-url="": Token redemption endpoint -redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" -request-logging=true: Log requests to stdout -scope="": Oauth scope specification -skip-auth-regex=: bypass authentication for requests path's that match (may be given multiple times) + -tls-cert="": path to certificate file + -tls-key="": path to private key file -upstream=: the http url(s) of the upstream endpoint. If multiple, routing is based on path -validate-url="": Access token validation endpoint -version=false: print version string @@ -130,10 +137,32 @@ See below for provider specific options The environment variables `OAUTH2_PROXY_CLIENT_ID`, `OAUTH2_PROXY_CLIENT_SECRET`, `OAUTH2_PROXY_COOKIE_SECRET`, `OAUTH2_PROXY_COOKIE_DOMAIN` and `OAUTH2_PROXY_COOKIE_EXPIRE` can be used in place of the corresponding command-line arguments. -### Example Nginx Configuration +## SSL Configuration -This example has a [Nginx](http://nginx.org/) SSL endpoint proxying to `oauth2_proxy` on port `4180`. -`oauth2_proxy` then authenticates requests for an upstream application running on port `8080`. The external +There are two recommended configurations. + +1) Configure SSL Terminiation with OAuth2 Proxy by providing a `--tls-cert=/path/to/cert.pem` and `--tls-key=/path/to/cert.key`. + +The command line to run `oauth2_proxy` in this configuration would look like this: + +```bash +./oauth2_proxy \ + --email-domain="yourcompany.com" \ + --upstream=http://127.0.0.1:8080/ \ + --tls-cert=/path/to/cert.pem \ + --tls-key=/path/to/cert.key \ + --cookie-secret=... \ + --cookie-secure=true \ + --provider=... \ + --client-id=... \ + --client-secret=... +``` + + +2) Configure SSL Termination with [Nginx](http://nginx.org/) (example config below) or Amazon ELB, or .... + +Nginx will listen on port `443` and handle SSL connections while proxying to `oauth2_proxy` on port `4180`. +`oauth2_proxy` which will then authenticate requests for an upstream application. The external endpoint for this example would be `https://internal.yourcompany.com/`. An example Nginx config follows. Note the use of `Strict-Transport-Security` header to pin requests to SSL @@ -159,7 +188,7 @@ server { } ``` -The command line to run `oauth2_proxy` would look like this: +The command line to run `oauth2_proxy` in this configuration would look like this: ```bash ./oauth2_proxy \ @@ -167,6 +196,7 @@ The command line to run `oauth2_proxy` would look like this: --upstream=http://127.0.0.1:8080/ \ --cookie-secret=... \ --cookie-secure=true \ + --provider=... \ --client-id=... \ --client-secret=... ``` @@ -174,7 +204,7 @@ The command line to run `oauth2_proxy` would look like this: ## Endpoint Documentation -OAuth2 Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated. +OAuth2 Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated. The `/oauth2` prefix can be changed with the `--proxy-prefix` config variable. * /robots.txt - returns a 200 OK response that disallows all User-agents from all paths; see [robotstxt.org](http://www.robotstxt.org/) for more info * /ping - returns an 200 OK response diff --git a/contrib/oauth2_proxy.cfg.example b/contrib/oauth2_proxy.cfg.example index 6e3f423..76320de 100644 --- a/contrib/oauth2_proxy.cfg.example +++ b/contrib/oauth2_proxy.cfg.example @@ -1,8 +1,13 @@ ## OAuth2 Proxy Config File ## https://github.com/bitly/oauth2_proxy -## : to listen on for HTTP clients +## : to listen on for HTTP/HTTPS clients # http_address = "127.0.0.1:4180" +# https_address = ":443" + +## TLS Settings +# tls_cert_file = "" +# tls_key_file = "" ## the OAuth Redirect URL. # defaults to the "https://" + requested host header + "/oauth2/callback" diff --git a/http.go b/http.go new file mode 100644 index 0000000..3b3d138 --- /dev/null +++ b/http.go @@ -0,0 +1,106 @@ +package main + +import ( + "crypto/tls" + "log" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +type Server struct { + Handler http.Handler + Opts *Options +} + +func (s *Server) ListenAndServe() { + if s.Opts.TLSKeyFile != "" || s.Opts.TLSCertFile != "" { + s.ServeHTTPS() + } else { + s.ServeHTTP() + } +} + +func (s *Server) ServeHTTP() { + u, err := url.Parse(s.Opts.HttpAddress) + if err != nil { + log.Fatalf("FATAL: could not parse %#v: %v", s.Opts.HttpAddress, err) + } + + var networkType string + switch u.Scheme { + case "", "http": + networkType = "tcp" + default: + networkType = u.Scheme + } + listenAddr := strings.TrimPrefix(u.String(), u.Scheme+"://") + + listener, err := net.Listen(networkType, listenAddr) + if err != nil { + log.Fatalf("FATAL: listen (%s, %s) failed - %s", networkType, listenAddr, err) + } + log.Printf("HTTP: listening on %s", listenAddr) + + server := &http.Server{Handler: s.Handler} + err = server.Serve(listener) + if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { + log.Printf("ERROR: http.Serve() - %s", err) + } + + log.Printf("HTTP: closing %s", listener.Addr()) +} + +func (s *Server) ServeHTTPS() { + addr := s.Opts.HttpsAddress + config := &tls.Config{ + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS12, + } + if config.NextProtos == nil { + config.NextProtos = []string{"http/1.1"} + } + + var err error + config.Certificates = make([]tls.Certificate, 1) + config.Certificates[0], err = tls.LoadX509KeyPair(s.Opts.TLSCertFile, s.Opts.TLSKeyFile) + if err != nil { + log.Fatalf("FATAL: loading tls config (%s, %s) failed - %s", s.Opts.TLSCertFile, s.Opts.TLSKeyFile, err) + } + + ln, err := net.Listen("tcp", addr) + if err != nil { + log.Fatalf("FATAL: listen (%s) failed - %s", addr, err) + } + log.Printf("HTTPS: listening on %s", ln.Addr()) + + tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config) + srv := &http.Server{Handler: s.Handler} + err = srv.Serve(tlsListener) + + if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { + log.Printf("ERROR: https.Serve() - %s", err) + } + + log.Printf("HTTPS: closing %s", tlsListener.Addr()) +} + +// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted +// connections. It's used by ListenAndServe and ListenAndServeTLS so +// dead TCP connections (e.g. closing laptop mid-download) eventually +// go away. +type tcpKeepAliveListener struct { + *net.TCPListener +} + +func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := ln.AcceptTCP() + if err != nil { + return + } + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil +} diff --git a/main.go b/main.go index 649efe0..5765432 100644 --- a/main.go +++ b/main.go @@ -4,9 +4,6 @@ import ( "flag" "fmt" "log" - "net" - "net/http" - "net/url" "os" "runtime" "strings" @@ -28,6 +25,9 @@ func main() { showVersion := flagSet.Bool("version", false, "print version string") flagSet.String("http-address", "127.0.0.1:4180", "[http://]: or unix:// to listen on for HTTP clients") + flagSet.String("https-address", ":443", ": to listen on for HTTPS clients") + flagSet.String("tls-cert", "", "path to certificate file") + flagSet.String("tls-key", "", "path to private key file") 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") @@ -57,7 +57,7 @@ func main() { flagSet.Bool("request-logging", true, "Log requests to stdout") - flagSet.String("provider", "", "Oauth provider (defaults to Google)") + flagSet.String("provider", "google", "OAuth provider") flagSet.String("login-url", "", "Authentication endpoint") flagSet.String("redeem-url", "", "Token redemption endpoint") flagSet.String("profile-url", "", "Profile access endpoint") @@ -109,31 +109,9 @@ func main() { } } - u, err := url.Parse(opts.HttpAddress) - if err != nil { - log.Fatalf("FATAL: could not parse %#v: %v", opts.HttpAddress, err) + s := &Server{ + Handler: LoggingHandler(os.Stdout, oauthproxy, opts.RequestLogging), + Opts: opts, } - - var networkType string - switch u.Scheme { - case "", "http": - networkType = "tcp" - default: - networkType = u.Scheme - } - listenAddr := strings.TrimPrefix(u.String(), u.Scheme+"://") - - listener, err := net.Listen(networkType, listenAddr) - if err != nil { - log.Fatalf("FATAL: listen (%s, %s) failed - %s", networkType, listenAddr, err) - } - log.Printf("listening on %s", listenAddr) - - server := &http.Server{Handler: LoggingHandler(os.Stdout, oauthproxy, opts.RequestLogging)} - err = server.Serve(listener) - if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { - log.Printf("ERROR: http.Serve() - %s", err) - } - - log.Printf("HTTP: closing %s", listener.Addr()) + s.ListenAndServe() } diff --git a/oauthproxy.go b/oauthproxy.go index fed5fb8..4567ff0 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -104,7 +104,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { redirectUrl := opts.redirectUrl redirectUrl.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) - log.Printf("OauthProxy configured for %s", opts.ClientID) + log.Printf("OauthProxy configured for %s Client ID: %s", opts.provider.Data().ProviderName, opts.ClientID) domain := opts.CookieDomain if domain == "" { domain = "" @@ -114,7 +114,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy { opts.CookieSecure = opts.CookieHttpsOnly } - log.Printf("Cookie settings: secure (https):%v httponly:%v expiry:%s domain:%s", opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain) + log.Printf("Cookie settings: name:%s secure (https):%v httponly:%v expiry:%s domain:%s", opts.CookieKey, opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain) var aes_cipher cipher.Block if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) { diff --git a/options.go b/options.go index 71dd7dd..4218af5 100644 --- a/options.go +++ b/options.go @@ -14,9 +14,12 @@ import ( type Options struct { ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy-prefix"` HttpAddress string `flag:"http-address" cfg:"http_address"` + HttpsAddress string `flag:"https-address" cfg:"https_address"` RedirectUrl string `flag:"redirect-url" cfg:"redirect_url"` ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"` ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"` + TLSCertFile string `flag:"tls-cert" cfg:"tls_cert_file"` + TLSKeyFile string `flag:"tls-key" cfg:"tls_key_file"` AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"` EmailDomains []string `flag:"email-domain" cfg:"email_domains"` @@ -63,6 +66,7 @@ func NewOptions() *Options { return &Options{ ProxyPrefix: "/oauth2", HttpAddress: "127.0.0.1:4180", + HttpsAddress: ":443", DisplayHtpasswdForm: true, CookieKey: "_oauthproxy", CookieHttpsOnly: true,