authenticate: remove extra login page (#34)

- Fixed a bug where Lifetime TTL was set to a minute.
- Remove nested mux in authenticate handlers.
- Remove extra ping endpoint in authenticate and proxy.
- Simplified sign in flow with multi-catch case statement.
- Removed debugging logging.
- Broke out cmd/pomerium options into own file.
- Renamed msicreant cipher to just cipher.

Closes #23
This commit is contained in:
Bobby DeSimone 2019-01-29 20:28:55 -08:00 committed by GitHub
parent bcecee5ee3
commit 236e5cd7de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 228 additions and 328 deletions

View file

@ -20,7 +20,6 @@ import (
var defaultOptions = &Options{ var defaultOptions = &Options{
CookieName: "_pomerium_authenticate", CookieName: "_pomerium_authenticate",
CookieHTTPOnly: true, CookieHTTPOnly: true,
SkipProviderButton: true,
CookieExpire: time.Duration(168) * time.Hour, CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(1) * time.Hour, CookieRefresh: time.Duration(1) * time.Hour,
SessionLifetimeTTL: time.Duration(720) * time.Hour, SessionLifetimeTTL: time.Duration(720) * time.Hour,
@ -58,7 +57,6 @@ type Options struct {
// issuing an Access Token. Named providers are already set with good defaults. // issuing an Access Token. Named providers are already set with good defaults.
// Most likely only overrides if using the generic OIDC provider. // Most likely only overrides if using the generic OIDC provider.
Scopes []string `envconfig:"IDP_SCOPE"` Scopes []string `envconfig:"IDP_SCOPE"`
SkipProviderButton bool `envconfig:"SKIP_PROVIDER_BUTTON"`
} }
// OptionsFromEnvConfig builds the authentication service's configuration // OptionsFromEnvConfig builds the authentication service's configuration
@ -80,7 +78,7 @@ func (o *Options) Validate() error {
} }
redirectPath := "/oauth2/callback" redirectPath := "/oauth2/callback"
if o.RedirectURL.Path != redirectPath { if o.RedirectURL.Path != redirectPath {
return fmt.Errorf("setting redirect-url was %s path should be %s", o.RedirectURL.Path, redirectPath) return fmt.Errorf("`setting` redirect-url was %s path should be %s", o.RedirectURL.Path, redirectPath)
} }
if o.ClientID == "" { if o.ClientID == "" {
return errors.New("missing setting: client id") return errors.New("missing setting: client id")
@ -127,8 +125,6 @@ type Authenticate struct {
sessionStore sessions.SessionStore sessionStore sessions.SessionStore
cipher cryptutil.Cipher cipher cryptutil.Cipher
skipProviderButton bool
provider providers.Provider provider providers.Provider
} }
@ -153,7 +149,7 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
return nil, err return nil, err
} }
cookieStore, err := sessions.NewCookieStore(opts.CookieName, cookieStore, err := sessions.NewCookieStore(opts.CookieName,
sessions.CreateMiscreantCookieCipher(decodedCookieSecret), sessions.CreateCookieCipher(decodedCookieSecret),
func(c *sessions.CookieStore) error { func(c *sessions.CookieStore) error {
c.CookieDomain = opts.CookieDomain c.CookieDomain = opts.CookieDomain
c.CookieHTTPOnly = opts.CookieHTTPOnly c.CookieHTTPOnly = opts.CookieHTTPOnly
@ -176,7 +172,6 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
csrfStore: cookieStore, csrfStore: cookieStore,
sessionStore: cookieStore, sessionStore: cookieStore,
cipher: cipher, cipher: cipher,
skipProviderButton: opts.SkipProviderButton,
} }
p.provider, err = newProvider(opts) p.provider, err = newProvider(opts)

View file

@ -32,15 +32,15 @@ func (p *Authenticate) Handler() http.Handler {
stdMiddleware := middleware.NewChain() stdMiddleware := middleware.NewChain()
stdMiddleware = stdMiddleware.Append(middleware.Healthcheck("/ping", version.UserAgent())) stdMiddleware = stdMiddleware.Append(middleware.Healthcheck("/ping", version.UserAgent()))
stdMiddleware = stdMiddleware.Append(middleware.NewHandler(log.Logger)) stdMiddleware = stdMiddleware.Append(middleware.NewHandler(log.Logger))
stdMiddleware = stdMiddleware.Append(middleware.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) { stdMiddleware = stdMiddleware.Append(middleware.AccessHandler(
// executed after handler route handler func(r *http.Request, status, size int, duration time.Duration) {
middleware.FromRequest(r).Info(). middleware.FromRequest(r).Debug().
Str("method", r.Method). Str("method", r.Method).
Str("url", r.URL.String()). Str("url", r.URL.String()).
Int("status", status). Int("status", status).
Int("size", size). Int("size", size).
Dur("duration", duration). Dur("duration", duration).
Msg("request") Msg("authenticate: request")
})) }))
stdMiddleware = stdMiddleware.Append(middleware.SetHeaders(securityHeaders)) stdMiddleware = stdMiddleware.Append(middleware.SetHeaders(securityHeaders))
stdMiddleware = stdMiddleware.Append(middleware.ForwardedAddrHandler("fwd_ip")) stdMiddleware = stdMiddleware.Append(middleware.ForwardedAddrHandler("fwd_ip"))
@ -55,29 +55,17 @@ func (p *Authenticate) Handler() http.Handler {
validateClientSecret := stdMiddleware.Append(middleware.ValidateClientSecret(p.SharedKey)) validateClientSecret := stdMiddleware.Append(middleware.ValidateClientSecret(p.SharedKey))
mux := http.NewServeMux() mux := http.NewServeMux()
// we setup global endpoints that should respond to any hostname mux.Handle("/robots.txt", stdMiddleware.ThenFunc(p.RobotsTxt))
mux.Handle("/ping", stdMiddleware.ThenFunc(p.PingPage)) // Identity Provider (IdP) callback endpoints and callbacks
serviceMux := http.NewServeMux() mux.Handle("/start", stdMiddleware.ThenFunc(p.OAuthStart))
// standard rest and healthcheck endpoints mux.Handle("/oauth2/callback", stdMiddleware.ThenFunc(p.OAuthCallback))
serviceMux.Handle("/ping", stdMiddleware.ThenFunc(p.PingPage)) // authenticate-server endpoints
serviceMux.Handle("/robots.txt", stdMiddleware.ThenFunc(p.RobotsTxt)) mux.Handle("/sign_in", validateSignatureMiddleware.ThenFunc(p.SignIn))
// Identity Provider (IdP) endpoints and callbacks mux.Handle("/sign_out", validateSignatureMiddleware.ThenFunc(p.SignOut)) // "GET", "POST"
serviceMux.Handle("/start", stdMiddleware.ThenFunc(p.OAuthStart)) mux.Handle("/profile", validateClientSecret.ThenFunc(p.GetProfile)) // GET
serviceMux.Handle("/oauth2/callback", stdMiddleware.ThenFunc(p.OAuthCallback)) mux.Handle("/validate", validateClientSecret.ThenFunc(p.ValidateToken)) // GET
// authenticator-server endpoints, todo(bdd): make gRPC mux.Handle("/redeem", validateClientSecret.ThenFunc(p.Redeem)) // POST
serviceMux.Handle("/sign_in", validateSignatureMiddleware.ThenFunc(p.SignIn)) mux.Handle("/refresh", validateClientSecret.ThenFunc(p.Refresh)) //POST
serviceMux.Handle("/sign_out", validateSignatureMiddleware.ThenFunc(p.SignOut)) // "GET", "POST"
serviceMux.Handle("/profile", validateClientSecret.ThenFunc(p.GetProfile)) // GET
serviceMux.Handle("/validate", validateClientSecret.ThenFunc(p.ValidateToken)) // GET
serviceMux.Handle("/redeem", validateClientSecret.ThenFunc(p.Redeem)) // POST
serviceMux.Handle("/refresh", validateClientSecret.ThenFunc(p.Refresh)) //POST
// NOTE: we have to include trailing slash for the router to match the host header
host := p.RedirectURL.Host
if !strings.HasSuffix(host, "/") {
host = fmt.Sprintf("%s/", host)
}
mux.Handle(host, serviceMux)
return mux return mux
} }
@ -88,16 +76,11 @@ func (p *Authenticate) RobotsTxt(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "User-agent: *\nDisallow: /") fmt.Fprintf(w, "User-agent: *\nDisallow: /")
} }
// PingPage handles the /ping route
func (p *Authenticate) PingPage(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "OK")
}
// SignInPage directs the user to the sign in page. Takes a `redirect_uri` param. // SignInPage directs the user to the sign in page. Takes a `redirect_uri` param.
func (p *Authenticate) SignInPage(w http.ResponseWriter, r *http.Request) { func (p *Authenticate) SignInPage(w http.ResponseWriter, r *http.Request) {
redirectURL := p.RedirectURL.ResolveReference(r.URL) redirectURL := p.RedirectURL.ResolveReference(r.URL)
destinationURL, _ := url.Parse(redirectURL.Query().Get("redirect_uri")) // checked by middleware
destinationURL, _ := url.Parse(redirectURL.Query().Get("redirect_uri"))
t := struct { t := struct {
ProviderName string ProviderName string
AllowedDomains []string AllowedDomains []string
@ -196,26 +179,17 @@ func (p *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) {
session, err := p.authenticate(w, r) session, err := p.authenticate(w, r)
switch err { switch err {
case nil: case nil:
// User is authenticated, redirect back to the proxy application // User is authenticated, redirect back to proxy
// with the necessary state
p.ProxyOAuthRedirect(w, r, session) p.ProxyOAuthRedirect(w, r, session)
case http.ErrNoCookie: case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession:
log.Error().Err(err).Msg("authenticate.SignIn : err no cookie") log.Debug().Err(err).Msg("authenticate.SignIn")
if p.skipProviderButton { if err != http.ErrNoCookie {
p.skipButtonOAuthStart(w, r)
} else {
p.SignInPage(w, r)
}
case sessions.ErrLifetimeExpired, sessions.ErrInvalidSession:
log.Error().Err(err).Msg("authenticate.SignIn")
p.sessionStore.ClearSession(w, r) p.sessionStore.ClearSession(w, r)
if p.skipProviderButton {
p.skipButtonOAuthStart(w, r)
} else {
p.SignInPage(w, r)
} }
p.OAuthStart(w, r)
default: default:
log.Error().Err(err).Msg("authenticate.SignIn : unknown error cookie") log.Error().Err(err).Msg("authenticate.SignIn")
httputil.ErrorResponse(w, r, err.Error(), httputil.CodeForError(err)) httputil.ErrorResponse(w, r, err.Error(), httputil.CodeForError(err))
} }
} }
@ -315,7 +289,6 @@ func (p *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
// SignOutPage renders a sign out page with a message // SignOutPage renders a sign out page with a message
func (p *Authenticate) SignOutPage(w http.ResponseWriter, r *http.Request, message string) { func (p *Authenticate) SignOutPage(w http.ResponseWriter, r *http.Request, message string) {
log.FromRequest(r).Debug().Msg("This is just a test to make sure signout works")
// validateRedirectURI middleware already ensures that this is a valid URL // validateRedirectURI middleware already ensures that this is a valid URL
redirectURI := r.Form.Get("redirect_uri") redirectURI := r.Form.Get("redirect_uri")
session, err := p.sessionStore.LoadSession(r) session, err := p.sessionStore.LoadSession(r)
@ -361,29 +334,24 @@ func (p *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest) httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
return return
} }
p.helperOAuthStart(w, r, authRedirectURL) authRedirectURL = p.RedirectURL.ResolveReference(r.URL)
}
func (p *Authenticate) skipButtonOAuthStart(w http.ResponseWriter, r *http.Request) {
p.helperOAuthStart(w, r, p.RedirectURL.ResolveReference(r.URL))
}
func (p *Authenticate) helperOAuthStart(w http.ResponseWriter, r *http.Request, authRedirectURL *url.URL) {
nonce := fmt.Sprintf("%x", cryptutil.GenerateKey()) nonce := fmt.Sprintf("%x", cryptutil.GenerateKey())
p.csrfStore.SetCSRF(w, r, nonce) p.csrfStore.SetCSRF(w, r, nonce)
// confirm the redirect uri is from the root domain
if !middleware.ValidRedirectURI(authRedirectURL.String(), p.ProxyRootDomains) { if !middleware.ValidRedirectURI(authRedirectURL.String(), p.ProxyRootDomains) {
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest) httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
return return
} }
// confirm proxy url is from the root domain
proxyRedirectURL, err := url.Parse(authRedirectURL.Query().Get("redirect_uri")) proxyRedirectURL, err := url.Parse(authRedirectURL.Query().Get("redirect_uri"))
if err != nil || !middleware.ValidRedirectURI(proxyRedirectURL.String(), p.ProxyRootDomains) { if err != nil || !middleware.ValidRedirectURI(proxyRedirectURL.String(), p.ProxyRootDomains) {
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest) httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
return return
} }
// get the signature and timestamp values then compare hmac
proxyRedirectSig := authRedirectURL.Query().Get("sig") proxyRedirectSig := authRedirectURL.Query().Get("sig")
ts := authRedirectURL.Query().Get("ts") ts := authRedirectURL.Query().Get("ts")
if !middleware.ValidSignature(proxyRedirectURL.String(), proxyRedirectSig, ts, p.SharedKey) { if !middleware.ValidSignature(proxyRedirectURL.String(), proxyRedirectSig, ts, p.SharedKey) {
@ -391,8 +359,8 @@ func (p *Authenticate) helperOAuthStart(w http.ResponseWriter, r *http.Request,
return return
} }
// embed authenticate service's state as the base64'd nonce and authenticate callback url
state := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", nonce, authRedirectURL.String()))) state := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", nonce, authRedirectURL.String())))
signInURL := p.provider.GetSignInURL(state) signInURL := p.provider.GetSignInURL(state)
http.Redirect(w, r, signInURL, http.StatusFound) http.Redirect(w, r, signInURL, http.StatusFound)

View file

@ -23,24 +23,6 @@ func testAuthenticate() *Authenticate {
return &auth return &auth
} }
func TestAuthenticate_PingPage(t *testing.T) {
auth := testAuthenticate()
req, err := http.NewRequest("GET", "/ping", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(auth.PingPage)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := "OK"
if rr.Body.String() != expected {
t.Errorf("handler returned wrong body: got %v want %v", rr.Body.String(), expected)
}
}
func TestAuthenticate_RobotsTxt(t *testing.T) { func TestAuthenticate_RobotsTxt(t *testing.T) {
auth := testAuthenticate() auth := testAuthenticate()
req, err := http.NewRequest("GET", "/robots.txt", nil) req, err := http.NewRequest("GET", "/robots.txt", nil)

View file

@ -6,7 +6,6 @@ import (
"net/http" "net/http"
"os" "os"
"github.com/pomerium/envconfig"
"github.com/pomerium/pomerium/authenticate" "github.com/pomerium/pomerium/authenticate"
"github.com/pomerium/pomerium/internal/https" "github.com/pomerium/pomerium/internal/https"
"github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/log"
@ -23,7 +22,7 @@ var (
func main() { func main() {
mainOpts, err := optionsFromEnvConfig() mainOpts, err := optionsFromEnvConfig()
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: failed to parse authenticator settings") log.Fatal().Err(err).Msg("cmd/pomerium: settings error")
} }
flag.Parse() flag.Parse()
if *debugFlag || mainOpts.Debug { if *debugFlag || mainOpts.Debug {
@ -33,51 +32,46 @@ func main() {
fmt.Printf("%s", version.FullVersion()) fmt.Printf("%s", version.FullVersion())
os.Exit(0) os.Exit(0)
} }
log.Debug().Str("version", version.FullVersion()).Str("user-agent", version.UserAgent()).Msg("cmd/pomerium") log.Info().Str("version", version.FullVersion()).Msg("cmd/pomerium")
var auth *authenticate.Authenticate var authenticateService *authenticate.Authenticate
var authHost string var authHost string
if mainOpts.Services == "all" || mainOpts.Services == "authenticate" { if mainOpts.Services == "all" || mainOpts.Services == "authenticate" {
authOpts, err := authenticate.OptionsFromEnvConfig() authOpts, err := authenticate.OptionsFromEnvConfig()
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: failed to parse authenticate settings") log.Fatal().Err(err).Msg("cmd/pomerium: authenticate settings")
} }
emailValidator := func(p *authenticate.Authenticate) error { emailValidator := func(p *authenticate.Authenticate) error {
p.Validator = options.NewEmailValidator(authOpts.AllowedDomains) p.Validator = options.NewEmailValidator(authOpts.AllowedDomains)
return nil return nil
} }
auth, err = authenticate.New(authOpts, emailValidator) authenticateService, err = authenticate.New(authOpts, emailValidator)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: failed to create authenticate") log.Fatal().Err(err).Msg("cmd/pomerium: new authenticate")
} }
authHost = authOpts.RedirectURL.Host authHost = authOpts.RedirectURL.Host
} }
var p *proxy.Proxy var proxyService *proxy.Proxy
if mainOpts.Services == "all" || mainOpts.Services == "proxy" { if mainOpts.Services == "all" || mainOpts.Services == "proxy" {
proxyOpts, err := proxy.OptionsFromEnvConfig() proxyOpts, err := proxy.OptionsFromEnvConfig()
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: failed to parse proxy settings") log.Fatal().Err(err).Msg("cmd/pomerium: proxy settings")
} }
p, err = proxy.New(proxyOpts) proxyService, err = proxy.New(proxyOpts)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: failed to create proxy") log.Fatal().Err(err).Msg("cmd/pomerium: new proxy")
} }
} }
topMux := http.NewServeMux() topMux := http.NewServeMux()
if auth != nil { if authenticateService != nil {
// Need to handle ping without host lookup for LB topMux.Handle(authHost+"/", authenticateService.Handler())
topMux.HandleFunc("/ping", func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusOK)
fmt.Fprintf(rw, "OK")
})
topMux.Handle(authHost+"/", auth.Handler())
} }
if p != nil { if proxyService != nil {
topMux.Handle("/", p.Handler()) topMux.Handle("/", proxyService.Handler())
} }
httpOpts := &https.Options{ httpOpts := &https.Options{
Addr: mainOpts.Addr, Addr: mainOpts.Addr,
@ -86,57 +80,5 @@ func main() {
CertFile: mainOpts.CertFile, CertFile: mainOpts.CertFile,
KeyFile: mainOpts.KeyFile, KeyFile: mainOpts.KeyFile,
} }
log.Fatal().Err(https.ListenAndServeTLS(httpOpts, topMux)).Msg("cmd/pomerium: fatal") log.Fatal().Err(https.ListenAndServeTLS(httpOpts, topMux)).Msg("cmd/pomerium: https serve failure")
}
// Options are the global environmental flags used to set up pomerium's services.
// If a base64 encoded certificate and key are not provided as environmental variables,
// or if a file location is not provided, the server will attempt to find a matching keypair
// in the local directory as `./cert.pem` and `./privkey.pem` respectively.
type Options struct {
// Debug enables more verbose logging, and outputs human-readable logs to Stdout.
// Set with POMERIUM_DEBUG
Debug bool `envconfig:"POMERIUM_DEBUG"`
// Services is a list enabled service mode. If none are selected, "all" is used.
// Available options are : "all", "authenticate", "proxy".
Services string `envconfig:"SERVICES"`
// Addr specifies the host and port on which the server should serve
// HTTPS requests. If empty, ":https" is used.
Addr string `envconfig:"ADDRESS"`
// Cert and Key specifies the base64 encoded TLS certificates to use.
Cert string `envconfig:"CERTIFICATE"`
Key string `envconfig:"CERTIFICATE_KEY"`
// CertFile and KeyFile specifies the TLS certificates to use.
CertFile string `envconfig:"CERTIFICATE_FILE"`
KeyFile string `envconfig:"CERTIFICATE_KEY_FILE"`
}
var defaultOptions = &Options{
Debug: false,
Services: "all",
}
// optionsFromEnvConfig builds the authentication service's configuration
// options from provided environmental variables
func optionsFromEnvConfig() (*Options, error) {
o := defaultOptions
if err := envconfig.Process("", o); err != nil {
return nil, err
}
if !isValidService(o.Services) {
return nil, fmt.Errorf("%s is an invalid service type", o.Services)
}
return o, nil
}
// isValidService checks to see if a service is a valid service mode
func isValidService(service string) bool {
switch service {
case
"all",
"proxy",
"authenticate":
return true
}
return false
} }

59
cmd/pomerium/options.go Normal file
View file

@ -0,0 +1,59 @@
package main // import "github.com/pomerium/pomerium/cmd/pomerium"
import (
"fmt"
"github.com/pomerium/envconfig"
)
// Options are the global environmental flags used to set up pomerium's services.
// If a base64 encoded certificate and key are not provided as environmental variables,
// or if a file location is not provided, the server will attempt to find a matching keypair
// in the local directory as `./cert.pem` and `./privkey.pem` respectively.
type Options struct {
// Debug enables more verbose logging, and outputs human-readable logs to Stdout.
// Set with POMERIUM_DEBUG
Debug bool `envconfig:"POMERIUM_DEBUG"`
// Services is a list enabled service mode. If none are selected, "all" is used.
// Available options are : "all", "authenticate", "proxy".
Services string `envconfig:"SERVICES"`
// Addr specifies the host and port on which the server should serve
// HTTPS requests. If empty, ":https" is used.
Addr string `envconfig:"ADDRESS"`
// Cert and Key specifies the base64 encoded TLS certificates to use.
Cert string `envconfig:"CERTIFICATE"`
Key string `envconfig:"CERTIFICATE_KEY"`
// CertFile and KeyFile specifies the TLS certificates to use.
CertFile string `envconfig:"CERTIFICATE_FILE"`
KeyFile string `envconfig:"CERTIFICATE_KEY_FILE"`
}
var defaultOptions = &Options{
Debug: false,
Services: "all",
}
// optionsFromEnvConfig builds the authentication service's configuration
// options from provided environmental variables
func optionsFromEnvConfig() (*Options, error) {
o := defaultOptions
if err := envconfig.Process("", o); err != nil {
return nil, err
}
if !isValidService(o.Services) {
return nil, fmt.Errorf("%s is an invalid service type", o.Services)
}
return o, nil
}
// isValidService checks to see if a service is a valid service mode
func isValidService(service string) bool {
switch service {
case
"all",
"proxy",
"authenticate":
return true
}
return false
}

View file

@ -24,7 +24,7 @@ function guideSidebar(title) {
{ {
title, title,
collapsable: false, collapsable: false,
children: ["", "docker", "kubernetes", "from-source"] children: ["", "kubernetes", "from-source"]
} }
]; ];
} }

View file

@ -13,7 +13,7 @@ Traditional [perimeter](https://www.redbooks.ibm.com/redpapers/pdfs/redp4397.pdf
- Failure to encapsulate a heterogeneous mix of cloud, on-premise, cloud, and multi-cloud environments. - Failure to encapsulate a heterogeneous mix of cloud, on-premise, cloud, and multi-cloud environments.
- User's don't like VPNs. - User's don't like VPNs.
Pomerium attempts to mitigate these shortcomings by by adopting the following principles. Pomerium attempts to mitigate these shortcomings by adopting the following principles.
- Trust flows from user, device, and context. - Trust flows from user, device, and context.
- Network location _does not impart trust_. Treat both internal and external networks as completely untrusted. - Network location _does not impart trust_. Treat both internal and external networks as completely untrusted.

View file

@ -1,48 +0,0 @@
# Docker
## Prerequisites
- A configured [identity provider]
## Install
Install [docker] and [docker-compose]. Docker-compose is a tool for defining and running multi-container Docker applications. We've created an example docker-compose file that creates a minimal, but complete test environnement for pomerium.
## Download
Copy and paste the contents of the provided example [basic.docker-compose.yml] and save it locally as `docker-compose.yml`.
## Configure
Edit the [docker-compose.yml] to match your [identity provider] settings.
Place your domain's wild-card TLS certificate next to the compose file. If you don't have one handy, the included [script] generates one from [LetsEncrypt].
## Run
You can then download the latest pomerium release of pomerium in docker form along some example containers and an nginx load balancer all in one step.
```bash
docker-compose up
```
Pomerium is configured to delegate access to two test apps [helloworld] and [httpbin].
## Navigate
Open a browser and navigate to `hello.your.domain.com` or `httpbin.your.domain.com`. You should see something like the following in your browser.
![Getting started](./get-started.gif)
And in your terminal.
[![asciicast](https://asciinema.org/a/tfbSWkUZgMRxHAQDqmcjjNwUg.svg)](https://asciinema.org/a/tfbSWkUZgMRxHAQDqmcjjNwUg)
[basic.docker-compose.yml]: ../docs/examples.html#basic-docker-compose-yml
[docker]: https://docs.docker.com/install/
[docker-compose]: (https://docs.docker.com/compose/install/)
[helloworld]: https://hub.docker.com/r/tutum/hello-world
[httpbin]: https://httpbin.org/
[identity provider]: ../docs/identity-providers.md
[letsencrypt]: https://letsencrypt.org/
[script]: https://github.com/pomerium/pomerium/blob/master/scripts/generate_wildcard_cert.sh

View file

@ -4,7 +4,7 @@
- Install [git](https://git-scm.com/) version control system - Install [git](https://git-scm.com/) version control system
- Install the [go](https://golang.org/doc/install) programming language - Install the [go](https://golang.org/doc/install) programming language
- A configured [identity provider]. - A configured [identity provider]
## Download ## Download

View file

@ -1 +1,48 @@
# Prerequisites # Docker
Docker and docker-compose are tools for defining and running multi-container Docker applications. We've created an example docker-compose file that creates a minimal, but complete test environment for pomerium.
## Prerequisites
- A configured [identity provider]
- Install [docker]
- Install [docker-compose]
## Download
Copy and paste the contents of the provided example [basic.docker-compose.yml] and save it locally as `docker-compose.yml`.
## Configure
Edit the `docker-compose.yml` to match your [identity provider] settings.
Place your domain's wild-card TLS certificate next to the compose file. If you don't have one handy, the included [script] generates one from [LetsEncrypt].
## Run
Docker-compose will automatically download the latest pomerium release as well as two example containers and an nginx load balancer all in one step.
```bash
docker-compose up
```
Pomerium is configured to delegate access to two test apps [helloworld] and [httpbin].
## Navigate
Open a browser and navigate to `hello.your.domain.com` or `httpbin.your.domain.com`. You should see something like the following in your browser.
![Getting started](./get-started.gif)
And in your terminal.
[![asciicast](https://asciinema.org/a/tfbSWkUZgMRxHAQDqmcjjNwUg.svg)](https://asciinema.org/a/tfbSWkUZgMRxHAQDqmcjjNwUg)
[basic.docker-compose.yml]: ../docs/examples.html#basic-docker-compose-yml
[docker]: https://docs.docker.com/install/
[docker-compose]: https://docs.docker.com/compose/install/
[helloworld]: https://hub.docker.com/r/tutum/hello-world
[httpbin]: https://httpbin.org/
[identity provider]: ../docs/identity-providers.md
[letsencrypt]: https://letsencrypt.org/
[script]: https://github.com/pomerium/pomerium/blob/master/scripts/generate_wildcard_cert.sh

View file

@ -40,12 +40,12 @@ type CookieStore struct {
SessionLifetimeTTL time.Duration SessionLifetimeTTL time.Duration
} }
// CreateMiscreantCookieCipher creates a new miscreant cipher with the cookie secret // CreateCookieCipher creates a new miscreant cipher with the cookie secret
func CreateMiscreantCookieCipher(cookieSecret []byte) func(s *CookieStore) error { func CreateCookieCipher(cookieSecret []byte) func(s *CookieStore) error {
return func(s *CookieStore) error { return func(s *CookieStore) error {
cipher, err := cryptutil.NewCipher(cookieSecret) cipher, err := cryptutil.NewCipher(cookieSecret)
if err != nil { if err != nil {
return fmt.Errorf("miscreant cookie-secret error: %s", err.Error()) return fmt.Errorf("cookie-secret error: %s", err.Error())
} }
s.CookieCipher = cipher s.CookieCipher = cipher
return nil return nil

View file

@ -13,7 +13,7 @@ import (
var testEncodedCookieSecret, _ = base64.StdEncoding.DecodeString("qICChm3wdjbjcWymm7PefwtPP6/PZv+udkFEubTeE38=") var testEncodedCookieSecret, _ = base64.StdEncoding.DecodeString("qICChm3wdjbjcWymm7PefwtPP6/PZv+udkFEubTeE38=")
func TestCreateMiscreantCookieCipher(t *testing.T) { func TestCreateCookieCipher(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
cookieSecret []byte cookieSecret []byte
@ -32,7 +32,7 @@ func TestCreateMiscreantCookieCipher(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
_, err := NewCookieStore("cookieName", CreateMiscreantCookieCipher(tc.cookieSecret)) _, err := NewCookieStore("cookieName", CreateCookieCipher(tc.cookieSecret))
if !tc.expectedError { if !tc.expectedError {
testutil.Ok(t, err) testutil.Ok(t, err)
} else { } else {
@ -309,7 +309,7 @@ func TestLoadCookiedSession(t *testing.T) {
}, },
{ {
name: "cookie set with cipher set", name: "cookie set with cipher set",
optFuncs: []func(*CookieStore) error{CreateMiscreantCookieCipher(testEncodedCookieSecret)}, optFuncs: []func(*CookieStore) error{CreateCookieCipher(testEncodedCookieSecret)},
setupCookies: func(t *testing.T, req *http.Request, s *CookieStore, sessionState *SessionState) { setupCookies: func(t *testing.T, req *http.Request, s *CookieStore, sessionState *SessionState) {
value, err := MarshalSession(sessionState, s.CookieCipher) value, err := MarshalSession(sessionState, s.CookieCipher)
testutil.Ok(t, err) testutil.Ok(t, err)
@ -323,7 +323,7 @@ func TestLoadCookiedSession(t *testing.T) {
}, },
{ {
name: "cookie set with invalid value cipher set", name: "cookie set with invalid value cipher set",
optFuncs: []func(*CookieStore) error{CreateMiscreantCookieCipher(testEncodedCookieSecret)}, optFuncs: []func(*CookieStore) error{CreateCookieCipher(testEncodedCookieSecret)},
setupCookies: func(t *testing.T, req *http.Request, s *CookieStore, sessionState *SessionState) { setupCookies: func(t *testing.T, req *http.Request, s *CookieStore, sessionState *SessionState) {
value := "574b776a7c934d6b9fc42ec63a389f79" value := "574b776a7c934d6b9fc42ec63a389f79"
req.AddCookie(s.makeSessionCookie(req, value, time.Hour, time.Now())) req.AddCookie(s.makeSessionCookie(req, value, time.Hour, time.Now()))

View file

@ -239,7 +239,7 @@ func (p *AuthenticateClient) ValidateSessionState(s *sessions.SessionState) bool
params.Add("shared_secret", p.SharedKey) params.Add("shared_secret", p.SharedKey)
req, err := p.newRequest("GET", fmt.Sprintf("%s?%s", p.ValidateURL.String(), params.Encode()), nil) req, err := p.newRequest("GET", fmt.Sprintf("%s?%s", p.ValidateURL.String(), params.Encode()), nil)
if err != nil { if err != nil {
log.Error().Err(err).Str("user", s.Email).Msg("proxy/authenticator.ValidateSessionState : error validating session state") log.Info().Err(err).Str("user", s.Email).Msg("proxy/authenticator: error validating session state")
return false return false
} }
req.Header.Set("X-Client-Secret", p.SharedKey) req.Header.Set("X-Client-Secret", p.SharedKey)
@ -248,7 +248,7 @@ func (p *AuthenticateClient) ValidateSessionState(s *sessions.SessionState) bool
resp, err := defaultHTTPClient.Do(req) resp, err := defaultHTTPClient.Do(req)
if err != nil { if err != nil {
log.Error().Err(err).Str("user", s.Email).Msg("proxy/authenticator.ValidateSessionState : error making request to validate access token") log.Info().Err(err).Str("user", s.Email).Msg("proxy/authenticator: error validating access token")
return false return false
} }
@ -260,16 +260,13 @@ func (p *AuthenticateClient) ValidateSessionState(s *sessions.SessionState) bool
s.ValidDeadline = extendDeadline(p.SessionValidTTL) s.ValidDeadline = extendDeadline(p.SessionValidTTL)
return true return true
} }
log.Info().Str("user", s.Email).Int("status-code", resp.StatusCode).Msg("proxy/authenticator.ValidateSessionState : could not validate user access token") log.Info().Str("user", s.Email).Int("status-code", resp.StatusCode).Msg("proxy/authenticator: bad status code")
return false return false
} }
s.ValidDeadline = extendDeadline(p.SessionValidTTL) s.ValidDeadline = extendDeadline(p.SessionValidTTL)
s.GracePeriodStart = time.Time{} s.GracePeriodStart = time.Time{}
log.Info().Str("user", s.Email).Msg("proxy/authenticator.ValidateSessionState : validated session")
return true return true
} }

View file

@ -29,6 +29,7 @@ var securityHeaders = map[string]string{
// Handler returns a http handler for an Proxy // Handler returns a http handler for an Proxy
func (p *Proxy) Handler() http.Handler { func (p *Proxy) Handler() http.Handler {
// routes
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/favicon.ico", p.Favicon) mux.HandleFunc("/favicon.ico", p.Favicon)
mux.HandleFunc("/robots.txt", p.RobotsTxt) mux.HandleFunc("/robots.txt", p.RobotsTxt)
@ -37,19 +38,12 @@ func (p *Proxy) Handler() http.Handler {
mux.HandleFunc("/.pomerium/auth", p.AuthenticateOnly) mux.HandleFunc("/.pomerium/auth", p.AuthenticateOnly)
mux.HandleFunc("/", p.Proxy) mux.HandleFunc("/", p.Proxy)
// Global middleware, which will be applied to each request in reverse // middleware chain
// order as applied here (i.e., we want to validate the host _first_ when
// processing a request)
var handler http.Handler = mux
// todo(bdd) : investigate if setting non-overridable headers makes sense
// handler = p.setResponseHeaderOverrides(handler)
// Middleware chain
c := middleware.NewChain() c := middleware.NewChain()
c = c.Append(middleware.Healthcheck("/ping", version.UserAgent())) c = c.Append(middleware.Healthcheck("/ping", version.UserAgent()))
c = c.Append(middleware.NewHandler(log.Logger)) c = c.Append(middleware.NewHandler(log.Logger))
c = c.Append(middleware.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) { c = c.Append(middleware.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
middleware.FromRequest(r).Info(). middleware.FromRequest(r).Debug().
Str("method", r.Method). Str("method", r.Method).
Str("url", r.URL.String()). Str("url", r.URL.String()).
Int("status", status). Int("status", status).
@ -57,7 +51,7 @@ func (p *Proxy) Handler() http.Handler {
Dur("duration", duration). Dur("duration", duration).
Str("pomerium-user", r.Header.Get(HeaderUserID)). Str("pomerium-user", r.Header.Get(HeaderUserID)).
Str("pomerium-email", r.Header.Get(HeaderEmail)). Str("pomerium-email", r.Header.Get(HeaderEmail)).
Msg("request") Msg("proxy: request")
})) }))
c = c.Append(middleware.SetHeaders(securityHeaders)) c = c.Append(middleware.SetHeaders(securityHeaders))
c = c.Append(middleware.RequireHTTPS) c = c.Append(middleware.RequireHTTPS)
@ -68,12 +62,7 @@ func (p *Proxy) Handler() http.Handler {
c = c.Append(middleware.RequestIDHandler("req_id", "Request-Id")) c = c.Append(middleware.RequestIDHandler("req_id", "Request-Id"))
c = c.Append(middleware.ValidateHost(p.mux)) c = c.Append(middleware.ValidateHost(p.mux))
h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip host validation for /ping requests because they hit the LB directly. mux.ServeHTTP(w, r)
if r.URL.Path == "/ping" {
p.PingPage(w, r)
return
}
handler.ServeHTTP(w, r)
})) }))
return h return h
} }
@ -97,12 +86,6 @@ func (p *Proxy) Favicon(w http.ResponseWriter, r *http.Request) {
p.Proxy(w, r) p.Proxy(w, r)
} }
// PingPage send back a 200 OK response.
func (p *Proxy) PingPage(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "OK")
}
// SignOut redirects the request to the sign out url. // SignOut redirects the request to the sign out url.
func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) { func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
p.sessionStore.ClearSession(w, r) p.sessionStore.ClearSession(w, r)
@ -266,28 +249,17 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
// OAuthStart. If successful, we proceed to proxy to the configured upstream. // OAuthStart. If successful, we proceed to proxy to the configured upstream.
if err != nil { if err != nil {
switch err { switch err {
case http.ErrNoCookie:
// No cookie is set, start the oauth flow
p.OAuthStart(w, r)
return
case ErrUserNotAuthorized: case ErrUserNotAuthorized:
// We know the user is not authorized for the request, we show them a forbidden page //todo(bdd) : custom forbidden page with details and troubleshooting info
p.ErrorPage(w, r, http.StatusForbidden, "Forbidden", "You're not authorized to view this page") log.FromRequest(r).Debug().Err(err).Msg("proxy: user access forbidden")
p.ErrorPage(w, r, http.StatusForbidden, "Forbidden", "You don't have access")
return return
case sessions.ErrLifetimeExpired: case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession:
// User's lifetime expired, we trigger the start of the oauth flow log.FromRequest(r).Debug().Err(err).Msg("proxy: starting auth flow")
p.OAuthStart(w, r)
return
case sessions.ErrInvalidSession:
// The user session is invalid and we can't decode it.
// This can happen for a variety of reasons but the most common non-malicious
// case occurs when the session encoding schema changes. We manage this ux
// by triggering the start of the oauth flow.
p.OAuthStart(w, r) p.OAuthStart(w, r)
return return
default: default:
log.FromRequest(r).Error().Err(err).Msg("unknown error") log.FromRequest(r).Error().Err(err).Msg("proxy: unexpected error")
// We don't know exactly what happened, but authenticating the user failed, show an error
p.ErrorPage(w, r, http.StatusInternalServerError, "Internal Error", "An unexpected error occurred") p.ErrorPage(w, r, http.StatusInternalServerError, "Internal Error", "An unexpected error occurred")
return return
} }
@ -315,69 +287,28 @@ func (p *Proxy) Authenticate(w http.ResponseWriter, r *http.Request) (err error)
session, err := p.sessionStore.LoadSession(r) session, err := p.sessionStore.LoadSession(r)
if err != nil { if err != nil {
// We loaded a cookie but it wasn't valid, clear it, and reject the request
log.FromRequest(r).Error().Err(err).Msg("error authenticating user")
return err return err
} }
// Lifetime period is the entire duration in which the session is valid.
// This should be set to something like 14 to 30 days.
if session.LifetimePeriodExpired() { if session.LifetimePeriodExpired() {
log.FromRequest(r).Warn().Str("user", session.Email).Msg("session lifetime has expired")
return sessions.ErrLifetimeExpired return sessions.ErrLifetimeExpired
} else if session.RefreshPeriodExpired() { } else if session.RefreshPeriodExpired() {
// Refresh period is the period in which the access token is valid. This is ultimately
// controlled by the upstream provider and tends to be around 1 hour.
ok, err := p.authenticateClient.RefreshSession(session) ok, err := p.authenticateClient.RefreshSession(session)
// We failed to refresh the session successfully
// clear the cookie and reject the request
if err != nil { if err != nil {
log.FromRequest(r).Error().Err(err).Str("user", session.Email).Msg("refreshing session failed")
return err return err
} }
if !ok { if !ok {
// User is not authorized after refresh
// clear the cookie and reject the request
log.FromRequest(r).Error().Str("user", session.Email).Msg("not authorized after refreshing session")
return ErrUserNotAuthorized return ErrUserNotAuthorized
} }
err = p.sessionStore.SaveSession(w, r, session)
if err != nil {
// We refreshed the session successfully, but failed to save it.
//
// This could be from failing to encode the session properly.
// But, we clear the session cookie and reject the request!
log.FromRequest(r).Error().Err(err).Str("user", session.Email).Msg("could not save refresh session")
return err
}
} else if session.ValidationPeriodExpired() { } else if session.ValidationPeriodExpired() {
// Validation period has expired, this is the shortest interval we use to
// check for valid requests. This should be set to something like a minute.
// This calls up the provider chain to validate this user is still active
// and hasn't been de-authorized.
ok := p.authenticateClient.ValidateSessionState(session) ok := p.authenticateClient.ValidateSessionState(session)
if !ok { if !ok {
// This user is now no longer authorized, or we failed to
// validate the user.
// Clear the cookie and reject the request
log.FromRequest(r).Error().Str("user", session.Email).Msg("no longer authorized after validation period")
return ErrUserNotAuthorized return ErrUserNotAuthorized
} }
}
err = p.sessionStore.SaveSession(w, r, session) err = p.sessionStore.SaveSession(w, r, session)
if err != nil { if err != nil {
// We validated the session successfully, but failed to save it.
// This could be from failing to encode the session properly.
// But, we clear the session cookie and reject the request!
log.FromRequest(r).Error().Err(err).Str("user", session.Email).Msg("could not save validated session")
return err return err
} }
}
r.Header.Set(HeaderUserID, session.User) r.Header.Set(HeaderUserID, session.User)
r.Header.Set(HeaderEmail, session.Email) r.Header.Set(HeaderEmail, session.Email)

27
proxy/handlers_test.go Normal file
View file

@ -0,0 +1,27 @@
package proxy
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestProxy_RobotsTxt(t *testing.T) {
auth := Proxy{}
req, err := http.NewRequest("GET", "/robots.txt", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(auth.RobotsTxt)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := fmt.Sprintf("User-agent: *\nDisallow: /")
if rr.Body.String() != expected {
t.Errorf("handler returned wrong body: got %v want %v", rr.Body.String(), expected)
}
}

View file

@ -161,7 +161,7 @@ func New(opts *Options) (*Proxy, error) {
} }
cookieStore, err := sessions.NewCookieStore(opts.CookieName, cookieStore, err := sessions.NewCookieStore(opts.CookieName,
sessions.CreateMiscreantCookieCipher(decodedSecret), sessions.CreateCookieCipher(decodedSecret),
func(c *sessions.CookieStore) error { func(c *sessions.CookieStore) error {
c.CookieDomain = opts.CookieDomain c.CookieDomain = opts.CookieDomain
c.CookieHTTPOnly = opts.CookieHTTPOnly c.CookieHTTPOnly = opts.CookieHTTPOnly
@ -177,8 +177,8 @@ func New(opts *Options) (*Proxy, error) {
opts.AuthenticateServiceURL, opts.AuthenticateServiceURL,
opts.SharedKey, opts.SharedKey,
// todo(bdd): fields below should be passed as function args // todo(bdd): fields below should be passed as function args
opts.SessionLifetimeTTL,
opts.SessionValidTTL, opts.SessionValidTTL,
opts.SessionLifetimeTTL,
opts.GracePeriodTTL, opts.GracePeriodTTL,
) )

View file

@ -23,7 +23,7 @@ kubectl create secret tls -n pomerium pomerium-tls --key privkey.pem --cert cert
# !!! IMPORTANT !!! # !!! IMPORTANT !!!
# YOU MUST CHANGE THE Identity Provider Client Secret # YOU MUST CHANGE THE Identity Provider Client Secret
# !!! IMPORTANT !!! # !!! IMPORTANT !!!
# kubectl create secret generic -n pomerium idp-client-secret --from-literal=REPLACE_ME # kubectl create secret generic -n pomerium idp-client-secret --from-literal=idp-client-secret=REPLACE_ME
# Create the proxy & authenticate deployment # Create the proxy & authenticate deployment
kubectl create -f docs/docs/examples/kubernetes/authenticate.deploy.yml kubectl create -f docs/docs/examples/kubernetes/authenticate.deploy.yml