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{
CookieName: "_pomerium_authenticate",
CookieHTTPOnly: true,
SkipProviderButton: true,
CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(1) * time.Hour,
SessionLifetimeTTL: time.Duration(720) * time.Hour,
@ -57,8 +56,7 @@ type Options struct {
// Scopes is an optional setting corresponding to OAuth 2.0 specification's access scopes
// issuing an Access Token. Named providers are already set with good defaults.
// Most likely only overrides if using the generic OIDC provider.
Scopes []string `envconfig:"IDP_SCOPE"`
SkipProviderButton bool `envconfig:"SKIP_PROVIDER_BUTTON"`
Scopes []string `envconfig:"IDP_SCOPE"`
}
// OptionsFromEnvConfig builds the authentication service's configuration
@ -80,7 +78,7 @@ func (o *Options) Validate() error {
}
redirectPath := "/oauth2/callback"
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 == "" {
return errors.New("missing setting: client id")
@ -127,8 +125,6 @@ type Authenticate struct {
sessionStore sessions.SessionStore
cipher cryptutil.Cipher
skipProviderButton bool
provider providers.Provider
}
@ -153,7 +149,7 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
return nil, err
}
cookieStore, err := sessions.NewCookieStore(opts.CookieName,
sessions.CreateMiscreantCookieCipher(decodedCookieSecret),
sessions.CreateCookieCipher(decodedCookieSecret),
func(c *sessions.CookieStore) error {
c.CookieDomain = opts.CookieDomain
c.CookieHTTPOnly = opts.CookieHTTPOnly
@ -167,16 +163,15 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
}
p := &Authenticate{
SharedKey: opts.SharedKey,
AllowedDomains: opts.AllowedDomains,
ProxyRootDomains: dotPrependDomains(opts.ProxyRootDomains),
CookieSecure: opts.CookieSecure,
RedirectURL: opts.RedirectURL,
templates: templates.New(),
csrfStore: cookieStore,
sessionStore: cookieStore,
cipher: cipher,
skipProviderButton: opts.SkipProviderButton,
SharedKey: opts.SharedKey,
AllowedDomains: opts.AllowedDomains,
ProxyRootDomains: dotPrependDomains(opts.ProxyRootDomains),
CookieSecure: opts.CookieSecure,
RedirectURL: opts.RedirectURL,
templates: templates.New(),
csrfStore: cookieStore,
sessionStore: cookieStore,
cipher: cipher,
}
p.provider, err = newProvider(opts)

View file

@ -32,16 +32,16 @@ func (p *Authenticate) Handler() http.Handler {
stdMiddleware := middleware.NewChain()
stdMiddleware = stdMiddleware.Append(middleware.Healthcheck("/ping", version.UserAgent()))
stdMiddleware = stdMiddleware.Append(middleware.NewHandler(log.Logger))
stdMiddleware = stdMiddleware.Append(middleware.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
// executed after handler route handler
middleware.FromRequest(r).Info().
Str("method", r.Method).
Str("url", r.URL.String()).
Int("status", status).
Int("size", size).
Dur("duration", duration).
Msg("request")
}))
stdMiddleware = stdMiddleware.Append(middleware.AccessHandler(
func(r *http.Request, status, size int, duration time.Duration) {
middleware.FromRequest(r).Debug().
Str("method", r.Method).
Str("url", r.URL.String()).
Int("status", status).
Int("size", size).
Dur("duration", duration).
Msg("authenticate: request")
}))
stdMiddleware = stdMiddleware.Append(middleware.SetHeaders(securityHeaders))
stdMiddleware = stdMiddleware.Append(middleware.ForwardedAddrHandler("fwd_ip"))
stdMiddleware = stdMiddleware.Append(middleware.RemoteAddrHandler("ip"))
@ -55,29 +55,17 @@ func (p *Authenticate) Handler() http.Handler {
validateClientSecret := stdMiddleware.Append(middleware.ValidateClientSecret(p.SharedKey))
mux := http.NewServeMux()
// we setup global endpoints that should respond to any hostname
mux.Handle("/ping", stdMiddleware.ThenFunc(p.PingPage))
serviceMux := http.NewServeMux()
// standard rest and healthcheck endpoints
serviceMux.Handle("/ping", stdMiddleware.ThenFunc(p.PingPage))
serviceMux.Handle("/robots.txt", stdMiddleware.ThenFunc(p.RobotsTxt))
// Identity Provider (IdP) endpoints and callbacks
serviceMux.Handle("/start", stdMiddleware.ThenFunc(p.OAuthStart))
serviceMux.Handle("/oauth2/callback", stdMiddleware.ThenFunc(p.OAuthCallback))
// authenticator-server endpoints, todo(bdd): make gRPC
serviceMux.Handle("/sign_in", validateSignatureMiddleware.ThenFunc(p.SignIn))
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)
mux.Handle("/robots.txt", stdMiddleware.ThenFunc(p.RobotsTxt))
// Identity Provider (IdP) callback endpoints and callbacks
mux.Handle("/start", stdMiddleware.ThenFunc(p.OAuthStart))
mux.Handle("/oauth2/callback", stdMiddleware.ThenFunc(p.OAuthCallback))
// authenticate-server endpoints
mux.Handle("/sign_in", validateSignatureMiddleware.ThenFunc(p.SignIn))
mux.Handle("/sign_out", validateSignatureMiddleware.ThenFunc(p.SignOut)) // "GET", "POST"
mux.Handle("/profile", validateClientSecret.ThenFunc(p.GetProfile)) // GET
mux.Handle("/validate", validateClientSecret.ThenFunc(p.ValidateToken)) // GET
mux.Handle("/redeem", validateClientSecret.ThenFunc(p.Redeem)) // POST
mux.Handle("/refresh", validateClientSecret.ThenFunc(p.Refresh)) //POST
return mux
}
@ -88,16 +76,11 @@ func (p *Authenticate) RobotsTxt(w http.ResponseWriter, r *http.Request) {
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.
func (p *Authenticate) SignInPage(w http.ResponseWriter, r *http.Request) {
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 {
ProviderName string
AllowedDomains []string
@ -196,26 +179,17 @@ func (p *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) {
session, err := p.authenticate(w, r)
switch err {
case nil:
// User is authenticated, redirect back to the proxy application
// with the necessary state
// User is authenticated, redirect back to proxy
p.ProxyOAuthRedirect(w, r, session)
case http.ErrNoCookie:
log.Error().Err(err).Msg("authenticate.SignIn : err no cookie")
if p.skipProviderButton {
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)
if p.skipProviderButton {
p.skipButtonOAuthStart(w, r)
} else {
p.SignInPage(w, r)
case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession:
log.Debug().Err(err).Msg("authenticate.SignIn")
if err != http.ErrNoCookie {
p.sessionStore.ClearSession(w, r)
}
p.OAuthStart(w, r)
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))
}
}
@ -315,7 +289,6 @@ func (p *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
// SignOutPage renders a sign out page with a message
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
redirectURI := r.Form.Get("redirect_uri")
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)
return
}
p.helperOAuthStart(w, r, authRedirectURL)
}
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) {
authRedirectURL = p.RedirectURL.ResolveReference(r.URL)
nonce := fmt.Sprintf("%x", cryptutil.GenerateKey())
p.csrfStore.SetCSRF(w, r, nonce)
// confirm the redirect uri is from the root domain
if !middleware.ValidRedirectURI(authRedirectURL.String(), p.ProxyRootDomains) {
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
return
}
// confirm proxy url is from the root domain
proxyRedirectURL, err := url.Parse(authRedirectURL.Query().Get("redirect_uri"))
if err != nil || !middleware.ValidRedirectURI(proxyRedirectURL.String(), p.ProxyRootDomains) {
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
return
}
// get the signature and timestamp values then compare hmac
proxyRedirectSig := authRedirectURL.Query().Get("sig")
ts := authRedirectURL.Query().Get("ts")
if !middleware.ValidSignature(proxyRedirectURL.String(), proxyRedirectSig, ts, p.SharedKey) {
@ -391,8 +359,8 @@ func (p *Authenticate) helperOAuthStart(w http.ResponseWriter, r *http.Request,
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())))
signInURL := p.provider.GetSignInURL(state)
http.Redirect(w, r, signInURL, http.StatusFound)

View file

@ -23,24 +23,6 @@ func testAuthenticate() *Authenticate {
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) {
auth := testAuthenticate()
req, err := http.NewRequest("GET", "/robots.txt", nil)

View file

@ -6,7 +6,6 @@ import (
"net/http"
"os"
"github.com/pomerium/envconfig"
"github.com/pomerium/pomerium/authenticate"
"github.com/pomerium/pomerium/internal/https"
"github.com/pomerium/pomerium/internal/log"
@ -23,7 +22,7 @@ var (
func main() {
mainOpts, err := optionsFromEnvConfig()
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()
if *debugFlag || mainOpts.Debug {
@ -33,51 +32,46 @@ func main() {
fmt.Printf("%s", version.FullVersion())
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
if mainOpts.Services == "all" || mainOpts.Services == "authenticate" {
authOpts, err := authenticate.OptionsFromEnvConfig()
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 {
p.Validator = options.NewEmailValidator(authOpts.AllowedDomains)
return nil
}
auth, err = authenticate.New(authOpts, emailValidator)
authenticateService, err = authenticate.New(authOpts, emailValidator)
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
}
var p *proxy.Proxy
var proxyService *proxy.Proxy
if mainOpts.Services == "all" || mainOpts.Services == "proxy" {
proxyOpts, err := proxy.OptionsFromEnvConfig()
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 {
log.Fatal().Err(err).Msg("cmd/pomerium: failed to create proxy")
log.Fatal().Err(err).Msg("cmd/pomerium: new proxy")
}
}
topMux := http.NewServeMux()
if auth != nil {
// Need to handle ping without host lookup for LB
topMux.HandleFunc("/ping", func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusOK)
fmt.Fprintf(rw, "OK")
})
topMux.Handle(authHost+"/", auth.Handler())
if authenticateService != nil {
topMux.Handle(authHost+"/", authenticateService.Handler())
}
if p != nil {
topMux.Handle("/", p.Handler())
if proxyService != nil {
topMux.Handle("/", proxyService.Handler())
}
httpOpts := &https.Options{
Addr: mainOpts.Addr,
@ -86,57 +80,5 @@ func main() {
CertFile: mainOpts.CertFile,
KeyFile: mainOpts.KeyFile,
}
log.Fatal().Err(https.ListenAndServeTLS(httpOpts, topMux)).Msg("cmd/pomerium: fatal")
}
// 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
log.Fatal().Err(https.ListenAndServeTLS(httpOpts, topMux)).Msg("cmd/pomerium: https serve failure")
}

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,
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.
- 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.
- 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 the [go](https://golang.org/doc/install) programming language
- A configured [identity provider].
- A configured [identity provider]
## 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
}
// CreateMiscreantCookieCipher creates a new miscreant cipher with the cookie secret
func CreateMiscreantCookieCipher(cookieSecret []byte) func(s *CookieStore) error {
// CreateCookieCipher creates a new miscreant cipher with the cookie secret
func CreateCookieCipher(cookieSecret []byte) func(s *CookieStore) error {
return func(s *CookieStore) error {
cipher, err := cryptutil.NewCipher(cookieSecret)
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
return nil

View file

@ -13,7 +13,7 @@ import (
var testEncodedCookieSecret, _ = base64.StdEncoding.DecodeString("qICChm3wdjbjcWymm7PefwtPP6/PZv+udkFEubTeE38=")
func TestCreateMiscreantCookieCipher(t *testing.T) {
func TestCreateCookieCipher(t *testing.T) {
testCases := []struct {
name string
cookieSecret []byte
@ -32,7 +32,7 @@ func TestCreateMiscreantCookieCipher(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := NewCookieStore("cookieName", CreateMiscreantCookieCipher(tc.cookieSecret))
_, err := NewCookieStore("cookieName", CreateCookieCipher(tc.cookieSecret))
if !tc.expectedError {
testutil.Ok(t, err)
} else {
@ -309,7 +309,7 @@ func TestLoadCookiedSession(t *testing.T) {
},
{
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) {
value, err := MarshalSession(sessionState, s.CookieCipher)
testutil.Ok(t, err)
@ -323,7 +323,7 @@ func TestLoadCookiedSession(t *testing.T) {
},
{
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) {
value := "574b776a7c934d6b9fc42ec63a389f79"
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)
req, err := p.newRequest("GET", fmt.Sprintf("%s?%s", p.ValidateURL.String(), params.Encode()), 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
}
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)
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
}
@ -260,16 +260,13 @@ func (p *AuthenticateClient) ValidateSessionState(s *sessions.SessionState) bool
s.ValidDeadline = extendDeadline(p.SessionValidTTL)
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
}
s.ValidDeadline = extendDeadline(p.SessionValidTTL)
s.GracePeriodStart = time.Time{}
log.Info().Str("user", s.Email).Msg("proxy/authenticator.ValidateSessionState : validated session")
return true
}

View file

@ -29,6 +29,7 @@ var securityHeaders = map[string]string{
// Handler returns a http handler for an Proxy
func (p *Proxy) Handler() http.Handler {
// routes
mux := http.NewServeMux()
mux.HandleFunc("/favicon.ico", p.Favicon)
mux.HandleFunc("/robots.txt", p.RobotsTxt)
@ -37,19 +38,12 @@ func (p *Proxy) Handler() http.Handler {
mux.HandleFunc("/.pomerium/auth", p.AuthenticateOnly)
mux.HandleFunc("/", p.Proxy)
// Global middleware, which will be applied to each request in reverse
// 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
// middleware chain
c := middleware.NewChain()
c = c.Append(middleware.Healthcheck("/ping", version.UserAgent()))
c = c.Append(middleware.NewHandler(log.Logger))
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("url", r.URL.String()).
Int("status", status).
@ -57,7 +51,7 @@ func (p *Proxy) Handler() http.Handler {
Dur("duration", duration).
Str("pomerium-user", r.Header.Get(HeaderUserID)).
Str("pomerium-email", r.Header.Get(HeaderEmail)).
Msg("request")
Msg("proxy: request")
}))
c = c.Append(middleware.SetHeaders(securityHeaders))
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.ValidateHost(p.mux))
h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip host validation for /ping requests because they hit the LB directly.
if r.URL.Path == "/ping" {
p.PingPage(w, r)
return
}
handler.ServeHTTP(w, r)
mux.ServeHTTP(w, r)
}))
return h
}
@ -97,12 +86,6 @@ func (p *Proxy) Favicon(w http.ResponseWriter, r *http.Request) {
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.
func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
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.
if err != nil {
switch err {
case http.ErrNoCookie:
// No cookie is set, start the oauth flow
p.OAuthStart(w, r)
return
case ErrUserNotAuthorized:
// We know the user is not authorized for the request, we show them a forbidden page
p.ErrorPage(w, r, http.StatusForbidden, "Forbidden", "You're not authorized to view this page")
//todo(bdd) : custom forbidden page with details and troubleshooting info
log.FromRequest(r).Debug().Err(err).Msg("proxy: user access forbidden")
p.ErrorPage(w, r, http.StatusForbidden, "Forbidden", "You don't have access")
return
case sessions.ErrLifetimeExpired:
// User's lifetime expired, we trigger the start of the oauth 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.
case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession:
log.FromRequest(r).Debug().Err(err).Msg("proxy: starting auth flow")
p.OAuthStart(w, r)
return
default:
log.FromRequest(r).Error().Err(err).Msg("unknown error")
// We don't know exactly what happened, but authenticating the user failed, show an error
log.FromRequest(r).Error().Err(err).Msg("proxy: unexpected error")
p.ErrorPage(w, r, http.StatusInternalServerError, "Internal Error", "An unexpected error occurred")
return
}
@ -315,69 +287,28 @@ func (p *Proxy) Authenticate(w http.ResponseWriter, r *http.Request) (err error)
session, err := p.sessionStore.LoadSession(r)
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
}
// 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() {
log.FromRequest(r).Warn().Str("user", session.Email).Msg("session lifetime has expired")
return sessions.ErrLifetimeExpired
} 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)
// We failed to refresh the session successfully
// clear the cookie and reject the request
if err != nil {
log.FromRequest(r).Error().Err(err).Str("user", session.Email).Msg("refreshing session failed")
return err
}
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
}
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() {
// 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)
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
}
err = p.sessionStore.SaveSession(w, r, session)
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
}
}
err = p.sessionStore.SaveSession(w, r, session)
if err != nil {
return err
}
r.Header.Set(HeaderUserID, session.User)
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,
sessions.CreateMiscreantCookieCipher(decodedSecret),
sessions.CreateCookieCipher(decodedSecret),
func(c *sessions.CookieStore) error {
c.CookieDomain = opts.CookieDomain
c.CookieHTTPOnly = opts.CookieHTTPOnly
@ -177,8 +177,8 @@ func New(opts *Options) (*Proxy, error) {
opts.AuthenticateServiceURL,
opts.SharedKey,
// todo(bdd): fields below should be passed as function args
opts.SessionLifetimeTTL,
opts.SessionValidTTL,
opts.SessionLifetimeTTL,
opts.GracePeriodTTL,
)

View file

@ -23,7 +23,7 @@ kubectl create secret tls -n pomerium pomerium-tls --key privkey.pem --cert cert
# !!! IMPORTANT !!!
# YOU MUST CHANGE THE Identity Provider Client Secret
# !!! 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
kubectl create -f docs/docs/examples/kubernetes/authenticate.deploy.yml