Added gif to the readme.

Simplified, and de-duplicated many of the configuration settings.
Removed configuration settings that could be deduced from other settings.
Added some basic documentation.
Removed the (duplicate?) user email domain validation check in proxy.
Removed the ClientID middleware check.
Added a shared key option to be used as a PSK instead of using the IDPs ClientID and ClientSecret.
Removed the CookieSecure setting as we only support secure.
Added a letsencrypt script to generate a wildcard certificate.
Removed the argument in proxy's constructor that allowed arbitrary fucntions to be passed in as validators.
Updated proxy's authenticator client to match the server implementation of just using a PSK.
Moved debug-mode logging into the log package.
Removed unused approval prompt setting.
Fixed a bug where identity provider urls were hardcoded.
Removed a bunch of unit tests. There have been so many changes many of these tests don't make sense and will need to be re-thought.
This commit is contained in:
Bobby DeSimone 2019-01-04 18:25:03 -08:00
parent 52a87b6e46
commit 90ab756de1
No known key found for this signature in database
GPG key ID: AEE4CF12FE86D07E
40 changed files with 409 additions and 1440 deletions

40
.gitignore vendored
View file

@ -38,25 +38,16 @@ _testmain.go
# Ruby # Ruby
website/vendor website/vendor
website/.bundle
website/build website/build
website/tmp website/tmp
# Vagrant
.vagrant/
Vagrantfile
# Configs
*.hcl
!command/agent/config/test-fixtures/config.hcl
!command/agent/config/test-fixtures/config-embedded-type.hcl
.DS_Store .DS_Store
.idea .idea
.vscode .vscode
dist/* dist/*
bin/*
tags tags
# Editor backups # Editor backups
@ -68,29 +59,22 @@ tags
*.ipr *.ipr
*.iml *.iml
# compiled output
ui/dist
ui/tmp
ui/root
http/bindata_assetfs.go
# dependencies # dependencies
ui/node_modules ui/node_modules
ui/bower_components ui/bower_components
# misc
ui/.DS_Store
ui/.sass-cache
ui/connect.lock
ui/coverage/*
ui/libpeerconnection.log
ui/npm-debug.log
ui/testem.log
# used for JS acceptance tests
ui/tests/helpers/vault-keys.js
ui/vault-ui-integration-server.pid
# for building static assets # for building static assets
node_modules node_modules
package-lock.json package-lock.json
# docs
lib/core/metadata.js
lib/core/MetadataBlog.js
translated_docs
build/
yarn.lock
node_modules
i18n/*

View file

@ -1,25 +1,29 @@
<img height="200" src="./docs/logo.png" alt="logo" align="right" > <img height="175" src="./docs/.vuepress/public/logo.svg" alt="logo" align="right" >
# Pomerium
# Pomerium : identity-aware access proxy
[![Travis CI](https://travis-ci.org/pomerium/pomerium.svg?branch=master)](https://travis-ci.org/pomerium/pomerium) [![Travis CI](https://travis-ci.org/pomerium/pomerium.svg?branch=master)](https://travis-ci.org/pomerium/pomerium)
[![Go Report Card](https://goreportcard.com/badge/github.com/pomerium/pomerium)](https://goreportcard.com/report/github.com/pomerium/pomerium) [![Go Report Card](https://goreportcard.com/badge/github.com/pomerium/pomerium)](https://goreportcard.com/report/github.com/pomerium/pomerium)
[![LICENSE](https://img.shields.io/github/license/pomerium/pomerium.svg?style=flat-square)](https://github.com/pomerium/pomerium/blob/master/LICENSE) [![LICENSE](https://img.shields.io/github/license/pomerium/pomerium.svg)](https://github.com/pomerium/pomerium/blob/master/LICENSE)
Pomerium is a tool for managing secure access to internal applications and resources. Pomerium is a tool for managing secure access to internal applications and resources.
Use Pomerium to: Use Pomerium to:
- provide a unified ingress gateway to internal corporate applications. - provide a unified gateway to internal corporate applications.
- enforce dynamic access policies based on context, identity, and device state. - enforce dynamic access policies based on context, identity, and device state.
- deploy mutually TLS (mTLS) encryption.
- aggregate logging and telemetry data. - aggregate logging and telemetry data.
To learn more about zero-trust / BeyondCorp, check out [awesome-zero-trust]. To learn more about zero-trust / BeyondCorp, check out [awesome-zero-trust].
## Getting started ## Get started
For instructions on getting started with Pomerium, see our getting started docs. For instructions on getting started with Pomerium, see our getting started docs.
## To start developing Pomerium <img src="./docs/.vuepress/public/getting-started.gif" alt="screen example" align="middle" >
## Start developing
Assuming you have a working [Go environment]. Assuming you have a working [Go environment].
@ -32,4 +36,4 @@ $ ./bin/pomerium -debug
``` ```
[awesome-zero-trust]: https://github.com/pomerium/awesome-zero-trust [awesome-zero-trust]: https://github.com/pomerium/awesome-zero-trust
[Go environment]: https://golang.org/doc/install [go environment]: https://golang.org/doc/install

View file

@ -17,57 +17,42 @@ import (
"github.com/pomerium/pomerium/internal/templates" "github.com/pomerium/pomerium/internal/templates"
) )
var defaultOptions = &Options{
CookieName: "_pomerium_authenticate",
CookieHTTPOnly: true,
CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(1) * time.Hour,
SessionLifetimeTTL: time.Duration(720) * time.Hour,
Scopes: []string{"openid", "email", "profile"},
}
// Options permits the configuration of the authentication service // Options permits the configuration of the authentication service
type Options struct { type Options struct {
// e.g. RedirectURL *url.URL `envconfig:"REDIRECT_URL" ` // e.g. auth.example.com/oauth/callback
Host string `envconfig:"HOST"`
// SharedKey string `envconfig:"SHARED_SECRET"`
ProxyClientID string `envconfig:"PROXY_CLIENT_ID"`
ProxyClientSecret string `envconfig:"PROXY_CLIENT_SECRET"`
// Coarse authorization based on user email domain // Coarse authorization based on user email domain
EmailDomains []string `envconfig:"SSO_EMAIL_DOMAIN"` AllowedDomains []string `envconfig:"ALLOWED_DOMAINS"`
ProxyRootDomains []string `envconfig:"PROXY_ROOT_DOMAIN"` ProxyRootDomains []string `envconfig:"PROXY_ROOT_DOMAIN"`
// Session/Cookie management // Session/Cookie management
CookieName string CookieName string
CookieSecret string `envconfig:"COOKIE_SECRET"` CookieSecret string `envconfig:"COOKIE_SECRET"`
CookieDomain string `envconfig:"COOKIE_DOMAIN"` CookieDomain string `envconfig:"COOKIE_DOMAIN"`
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE" default:"168h"` CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH" default:"1h"` CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
CookieSecure bool `envconfig:"COOKIE_SECURE" default:"true"` CookieSecure bool `envconfig:"COOKIE_SECURE"`
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY" default:"true"` CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
AuthCodeSecret string `envconfig:"AUTH_CODE_SECRET"` SessionLifetimeTTL time.Duration `envconfig:"SESSION_LIFETIME_TTL"`
SessionLifetimeTTL time.Duration `envconfig:"SESSION_LIFETIME_TTL" default:"720h"`
// Authentication provider configuration vars // Authentication provider configuration vars
RedirectURL *url.URL `envconfig:"IDP_REDIRECT_URL" ` // e.g. auth.example.com/oauth/callback
ClientID string `envconfig:"IDP_CLIENT_ID"` // IdP ClientID ClientID string `envconfig:"IDP_CLIENT_ID"` // IdP ClientID
ClientSecret string `envconfig:"IDP_CLIENT_SECRET"` // IdP Secret ClientSecret string `envconfig:"IDP_CLIENT_SECRET"` // IdP Secret
Provider string `envconfig:"IDP_PROVIDER"` //Provider name e.g. "oidc","okta","google",etc Provider string `envconfig:"IDP_PROVIDER"` //Provider name e.g. "oidc","okta","google",etc
ProviderURL *url.URL `envconfig:"IDP_PROVIDER_URL"` ProviderURL string `envconfig:"IDP_PROVIDER_URL"`
Scopes []string `envconfig:"IDP_SCOPE" default:"openid,email,profile"` Scopes []string `envconfig:"IDP_SCOPE" default:"openid,email,profile"`
// todo(bdd) : can delete?`
ApprovalPrompt string `envconfig:"IDP_APPROVAL_PROMPT" default:"consent"`
RequestLogging bool `envconfig:"REQUEST_LOGGING" default:"true"`
RequestTimeout time.Duration `envconfig:"REQUEST_TIMEOUT" default:"2s"`
}
var defaultOptions = &Options{
EmailDomains: []string{"*"},
CookieName: "_pomerium_authenticate",
CookieSecure: true,
CookieHTTPOnly: true,
CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(1) * time.Hour,
RequestTimeout: time.Duration(2) * time.Second,
SessionLifetimeTTL: time.Duration(720) * time.Hour,
ApprovalPrompt: "consent",
Scopes: []string{"openid", "email", "profile"},
} }
// OptionsFromEnvConfig builds the authentication service's configuration // OptionsFromEnvConfig builds the authentication service's configuration
@ -84,9 +69,7 @@ func OptionsFromEnvConfig() (*Options, error) {
// The checks do not modify the internal state of the Option structure. Function returns // The checks do not modify the internal state of the Option structure. Function returns
// on first error found. // on first error found.
func (o *Options) Validate() error { func (o *Options) Validate() error {
if o.ProviderURL == nil {
return errors.New("missing setting: identity provider url")
}
if o.RedirectURL == nil { if o.RedirectURL == nil {
return errors.New("missing setting: identity provider redirect url") return errors.New("missing setting: identity provider redirect url")
} }
@ -100,17 +83,14 @@ func (o *Options) Validate() error {
if o.ClientSecret == "" { if o.ClientSecret == "" {
return errors.New("missing setting: client secret") return errors.New("missing setting: client secret")
} }
if len(o.EmailDomains) == 0 { if len(o.AllowedDomains) == 0 {
return errors.New("missing setting email domain") return errors.New("missing setting email domain")
} }
if len(o.ProxyRootDomains) == 0 { if len(o.ProxyRootDomains) == 0 {
return errors.New("missing setting: proxy root domain") return errors.New("missing setting: proxy root domain")
} }
if o.ProxyClientID == "" { if o.SharedKey == "" {
return errors.New("missing setting: proxy client id") return errors.New("missing setting: shared secret")
}
if o.ProxyClientSecret == "" {
return errors.New("missing setting: proxy client secret")
} }
decodedCookieSecret, err := base64.StdEncoding.DecodeString(o.CookieSecret) decodedCookieSecret, err := base64.StdEncoding.DecodeString(o.CookieSecret)
@ -140,15 +120,15 @@ func (o *Options) Validate() error {
// Authenticator stores all the information associated with proxying the request. // Authenticator stores all the information associated with proxying the request.
type Authenticator struct { type Authenticator struct {
RedirectURL *url.URL
Validator func(string) bool Validator func(string) bool
EmailDomains []string AllowedDomains []string
ProxyRootDomains []string ProxyRootDomains []string
Host string
CookieSecure bool CookieSecure bool
ProxyClientID string SharedKey string
ProxyClientSecret string
SessionLifetimeTTL time.Duration SessionLifetimeTTL time.Duration
@ -159,7 +139,6 @@ type Authenticator struct {
sessionStore sessions.SessionStore sessionStore sessions.SessionStore
cipher aead.Cipher cipher aead.Cipher
redirectURL *url.URL
provider providers.Provider provider providers.Provider
} }
@ -171,7 +150,7 @@ func NewAuthenticator(opts *Options, optionFuncs ...func(*Authenticator) error)
if err := opts.Validate(); err != nil { if err := opts.Validate(); err != nil {
return nil, err return nil, err
} }
decodedAuthCodeSecret, err := base64.StdEncoding.DecodeString(opts.AuthCodeSecret) decodedAuthCodeSecret, err := base64.StdEncoding.DecodeString(opts.CookieSecret)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -198,12 +177,11 @@ func NewAuthenticator(opts *Options, optionFuncs ...func(*Authenticator) error)
} }
p := &Authenticator{ p := &Authenticator{
ProxyClientID: opts.ProxyClientID, SharedKey: opts.SharedKey,
ProxyClientSecret: opts.ProxyClientSecret, AllowedDomains: opts.AllowedDomains,
EmailDomains: opts.EmailDomains,
ProxyRootDomains: dotPrependDomains(opts.ProxyRootDomains), ProxyRootDomains: dotPrependDomains(opts.ProxyRootDomains),
CookieSecure: opts.CookieSecure, CookieSecure: opts.CookieSecure,
redirectURL: opts.RedirectURL, RedirectURL: opts.RedirectURL,
templates: templates.New(), templates: templates.New(),
csrfStore: cookieStore, csrfStore: cookieStore,
sessionStore: cookieStore, sessionStore: cookieStore,
@ -229,11 +207,10 @@ func newProvider(opts *Options) (providers.Provider, error) {
pd := &providers.ProviderData{ pd := &providers.ProviderData{
RedirectURL: opts.RedirectURL, RedirectURL: opts.RedirectURL,
ProviderName: opts.Provider, ProviderName: opts.Provider,
ProviderURL: opts.ProviderURL,
ClientID: opts.ClientID, ClientID: opts.ClientID,
ClientSecret: opts.ClientSecret, ClientSecret: opts.ClientSecret,
ApprovalPrompt: opts.ApprovalPrompt,
SessionLifetimeTTL: opts.SessionLifetimeTTL, SessionLifetimeTTL: opts.SessionLifetimeTTL,
ProviderURL: opts.ProviderURL,
Scopes: opts.Scopes, Scopes: opts.Scopes,
} }
np, err := providers.New(opts.Provider, pd) np, err := providers.New(opts.Provider, pd)

File diff suppressed because it is too large Load diff

View file

@ -40,7 +40,7 @@ func (p *Authenticator) Handler() http.Handler {
serviceMux.HandleFunc("/start", m.WithMethods(p.OAuthStart, "GET")) serviceMux.HandleFunc("/start", m.WithMethods(p.OAuthStart, "GET"))
serviceMux.HandleFunc("/oauth2/callback", m.WithMethods(p.OAuthCallback, "GET")) serviceMux.HandleFunc("/oauth2/callback", m.WithMethods(p.OAuthCallback, "GET"))
// authenticator-server endpoints, todo(bdd): make gRPC // authenticator-server endpoints, todo(bdd): make gRPC
serviceMux.HandleFunc("/sign_in", m.WithMethods(m.ValidateClientID(p.validateSignature(p.SignIn), p.ProxyClientID), "GET")) serviceMux.HandleFunc("/sign_in", m.WithMethods(p.validateSignature(p.SignIn), "GET"))
serviceMux.HandleFunc("/sign_out", m.WithMethods(p.validateSignature(p.SignOut), "GET", "POST")) serviceMux.HandleFunc("/sign_out", m.WithMethods(p.validateSignature(p.SignOut), "GET", "POST"))
serviceMux.HandleFunc("/profile", m.WithMethods(p.validateExisting(p.GetProfile), "GET")) serviceMux.HandleFunc("/profile", m.WithMethods(p.validateExisting(p.GetProfile), "GET"))
serviceMux.HandleFunc("/validate", m.WithMethods(p.validateExisting(p.ValidateToken), "GET")) serviceMux.HandleFunc("/validate", m.WithMethods(p.validateExisting(p.ValidateToken), "GET"))
@ -48,7 +48,7 @@ func (p *Authenticator) Handler() http.Handler {
serviceMux.HandleFunc("/refresh", m.WithMethods(p.validateExisting(p.Refresh), "POST")) serviceMux.HandleFunc("/refresh", m.WithMethods(p.validateExisting(p.Refresh), "POST"))
// NOTE: we have to include trailing slash for the router to match the host header // NOTE: we have to include trailing slash for the router to match the host header
host := p.Host host := p.RedirectURL.Host
if !strings.HasSuffix(host, "/") { if !strings.HasSuffix(host, "/") {
host = fmt.Sprintf("%s/", host) host = fmt.Sprintf("%s/", host)
} }
@ -59,14 +59,14 @@ func (p *Authenticator) Handler() http.Handler {
// validateSignature wraps a common collection of middlewares to validate signatures // validateSignature wraps a common collection of middlewares to validate signatures
func (p *Authenticator) validateSignature(f http.HandlerFunc) http.HandlerFunc { func (p *Authenticator) validateSignature(f http.HandlerFunc) http.HandlerFunc {
return validateRedirectURI(validateSignature(f, p.ProxyClientSecret), p.ProxyRootDomains) return validateRedirectURI(validateSignature(f, p.SharedKey), p.ProxyRootDomains)
} }
// validateSignature wraps a common collection of middlewares to validate // validateSignature wraps a common collection of middlewares to validate
// a (presumably) existing user session // a (presumably) existing user session
func (p *Authenticator) validateExisting(f http.HandlerFunc) http.HandlerFunc { func (p *Authenticator) validateExisting(f http.HandlerFunc) http.HandlerFunc {
return m.ValidateClientID(m.ValidateClientSecret(f, p.ProxyClientSecret), p.ProxyClientID) return m.ValidateClientSecret(f, p.SharedKey)
} }
// RobotsTxt handles the /robots.txt route. // RobotsTxt handles the /robots.txt route.
@ -85,18 +85,18 @@ func (p *Authenticator) PingPage(rw http.ResponseWriter, req *http.Request) {
func (p *Authenticator) SignInPage(rw http.ResponseWriter, req *http.Request, code int) { func (p *Authenticator) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
requestLog := log.WithRequest(req, "authenticate.SignInPage") requestLog := log.WithRequest(req, "authenticate.SignInPage")
rw.WriteHeader(code) rw.WriteHeader(code)
redirectURL := p.redirectURL.ResolveReference(req.URL) redirectURL := p.RedirectURL.ResolveReference(req.URL)
// validateRedirectURI middleware already ensures that this is a valid URL // validateRedirectURI middleware already ensures that this is a valid URL
destinationURL, _ := url.Parse(redirectURL.Query().Get("redirect_uri")) destinationURL, _ := url.Parse(redirectURL.Query().Get("redirect_uri"))
t := struct { t := struct {
ProviderName string ProviderName string
EmailDomains []string AllowedDomains []string
Redirect string Redirect string
Destination string Destination string
Version string Version string
}{ }{
ProviderName: p.provider.Data().ProviderName, ProviderName: p.provider.Data().ProviderName,
EmailDomains: p.EmailDomains, AllowedDomains: p.AllowedDomains,
Redirect: redirectURL.String(), Redirect: redirectURL.String(),
Destination: destinationURL.Host, Destination: destinationURL.Host,
Version: version.FullVersion(), Version: version.FullVersion(),
@ -105,7 +105,7 @@ func (p *Authenticator) SignInPage(rw http.ResponseWriter, req *http.Request, co
Str("ProviderName", p.provider.Data().ProviderName). Str("ProviderName", p.provider.Data().ProviderName).
Str("Redirect", redirectURL.String()). Str("Redirect", redirectURL.String()).
Str("Destination", destinationURL.Host). Str("Destination", destinationURL.Host).
Str("EmailDomains", strings.Join(p.EmailDomains, ", ")). Str("AllowedDomains", strings.Join(p.AllowedDomains, ", ")).
Msg("authenticate.SignInPage") Msg("authenticate.SignInPage")
p.templates.ExecuteTemplate(rw, "sign_in.html", t) p.templates.ExecuteTemplate(rw, "sign_in.html", t)
} }
@ -358,7 +358,7 @@ func (p *Authenticator) OAuthStart(rw http.ResponseWriter, req *http.Request) {
proxyRedirectSig := authRedirectURL.Query().Get("sig") proxyRedirectSig := authRedirectURL.Query().Get("sig")
ts := authRedirectURL.Query().Get("ts") ts := authRedirectURL.Query().Get("ts")
if !validSignature(proxyRedirectURL.String(), proxyRedirectSig, ts, p.ProxyClientSecret) { if !validSignature(proxyRedirectURL.String(), proxyRedirectSig, ts, p.SharedKey) {
httputil.ErrorResponse(rw, req, "Invalid redirect parameter", http.StatusBadRequest) httputil.ErrorResponse(rw, req, "Invalid redirect parameter", http.StatusBadRequest)
return return
} }

View file

@ -46,7 +46,7 @@ func validRedirectURI(uri string, rootDomains []string) bool {
return false return false
} }
func validateSignature(f http.HandlerFunc, proxyClientSecret string) http.HandlerFunc { func validateSignature(f http.HandlerFunc, sharedKey string) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) { return func(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm() err := req.ParseForm()
if err != nil { if err != nil {
@ -56,7 +56,7 @@ func validateSignature(f http.HandlerFunc, proxyClientSecret string) http.Handle
redirectURI := req.Form.Get("redirect_uri") redirectURI := req.Form.Get("redirect_uri")
sigVal := req.Form.Get("sig") sigVal := req.Form.Get("sig")
timestamp := req.Form.Get("ts") timestamp := req.Form.Get("ts")
if !validSignature(redirectURI, sigVal, timestamp, proxyClientSecret) { if !validSignature(redirectURI, sigVal, timestamp, sharedKey) {
httputil.ErrorResponse(rw, req, "Invalid redirect parameter", http.StatusBadRequest) httputil.ErrorResponse(rw, req, "Invalid redirect parameter", http.StatusBadRequest)
return return
} }

View file

@ -14,6 +14,8 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
const defaultGoogleProviderURL = "https://accounts.google.com"
// GoogleProvider is an implementation of the Provider interface. // GoogleProvider is an implementation of the Provider interface.
type GoogleProvider struct { type GoogleProvider struct {
*ProviderData *ProviderData
@ -25,7 +27,11 @@ type GoogleProvider struct {
// NewGoogleProvider returns a new GoogleProvider and sets the provider url endpoints. // NewGoogleProvider returns a new GoogleProvider and sets the provider url endpoints.
func NewGoogleProvider(p *ProviderData) (*GoogleProvider, error) { func NewGoogleProvider(p *ProviderData) (*GoogleProvider, error) {
ctx := context.Background() ctx := context.Background()
provider, err := oidc.NewProvider(ctx, "https://accounts.google.com")
if p.ProviderURL == "" {
p.ProviderURL = defaultGoogleProviderURL
}
provider, err := oidc.NewProvider(ctx, p.ProviderURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -2,6 +2,7 @@ package providers // import "github.com/pomerium/pomerium/internal/providers"
import ( import (
"context" "context"
"errors"
oidc "github.com/pomerium/go-oidc" oidc "github.com/pomerium/go-oidc"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -16,7 +17,10 @@ type OIDCProvider struct {
// NewOIDCProvider creates a new instance of an OpenID Connect provider. // NewOIDCProvider creates a new instance of an OpenID Connect provider.
func NewOIDCProvider(p *ProviderData) (*OIDCProvider, error) { func NewOIDCProvider(p *ProviderData) (*OIDCProvider, error) {
ctx := context.Background() ctx := context.Background()
provider, err := oidc.NewProvider(ctx, "https://accounts.google.com") if p.ProviderURL == "" {
return nil, errors.New("missing required provider url")
}
provider, err := oidc.NewProvider(ctx, p.ProviderURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -2,6 +2,7 @@ package providers // import "github.com/pomerium/pomerium/internal/providers"
import ( import (
"context" "context"
"errors"
"net/url" "net/url"
oidc "github.com/pomerium/go-oidc" oidc "github.com/pomerium/go-oidc"
@ -23,7 +24,10 @@ type OktaProvider struct {
// NewOktaProvider creates a new instance of an OpenID Connect provider. // NewOktaProvider creates a new instance of an OpenID Connect provider.
func NewOktaProvider(p *ProviderData) (*OktaProvider, error) { func NewOktaProvider(p *ProviderData) (*OktaProvider, error) {
ctx := context.Background() ctx := context.Background()
provider, err := oidc.NewProvider(ctx, "https://dev-108295.oktapreview.com/oauth2/default") if p.ProviderURL == "" {
return nil, errors.New("missing required provider url")
}
provider, err := oidc.NewProvider(ctx, p.ProviderURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -45,7 +45,6 @@ func New(provider string, p *ProviderData) (Provider, error) {
} }
return p, nil return p, nil
case OktaProviderName: case OktaProviderName:
log.Info().Msg("Okta!")
p, err := NewOktaProvider(p) p, err := NewOktaProvider(p)
if err != nil { if err != nil {
return nil, err return nil, err
@ -67,9 +66,8 @@ type ProviderData struct {
ProviderName string ProviderName string
ClientID string ClientID string
ClientSecret string ClientSecret string
ProviderURL *url.URL ProviderURL string
Scopes []string Scopes []string
ApprovalPrompt string
SessionLifetimeTTL time.Duration SessionLifetimeTTL time.Duration
verifier *oidc.IDTokenVerifier verifier *oidc.IDTokenVerifier
@ -227,7 +225,7 @@ func (p *ProviderData) RefreshAccessToken(refreshToken string) (string, time.Dur
c := oauth2.Config{ c := oauth2.Config{
ClientID: p.ClientID, ClientID: p.ClientID,
ClientSecret: p.ClientSecret, ClientSecret: p.ClientSecret,
Endpoint: oauth2.Endpoint{TokenURL: p.ProviderURL.String()}, Endpoint: oauth2.Endpoint{TokenURL: p.ProviderURL},
} }
t := oauth2.Token{RefreshToken: refreshToken} t := oauth2.Token{RefreshToken: refreshToken}
ts := c.TokenSource(ctx, &t) ts := c.TokenSource(ctx, &t)

View file

@ -27,14 +27,15 @@ type TestProvider struct {
// NewTestProvider creates a new mock test provider. // NewTestProvider creates a new mock test provider.
func NewTestProvider(providerURL *url.URL) *TestProvider { func NewTestProvider(providerURL *url.URL) *TestProvider {
return &TestProvider{ host := &url.URL{
ProviderData: &ProviderData{
ProviderName: "Test Provider",
ProviderURL: &url.URL{
Scheme: "http", Scheme: "http",
Host: providerURL.Host, Host: providerURL.Host,
Path: "/authorize", Path: "/authorize",
}, }
return &TestProvider{
ProviderData: &ProviderData{
ProviderName: "Test Provider",
ProviderURL: host.String(),
}, },
} }
} }

View file

@ -6,13 +6,12 @@ import (
"net/http" "net/http"
"os" "os"
"github.com/rs/zerolog"
"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"
"github.com/pomerium/pomerium/internal/options" "github.com/pomerium/pomerium/internal/options"
"github.com/pomerium/pomerium/internal/version" "github.com/pomerium/pomerium/internal/version"
"github.com/pomerium/pomerium/authenticate"
"github.com/pomerium/pomerium/proxy" "github.com/pomerium/pomerium/proxy"
) )
@ -24,7 +23,7 @@ var (
func main() { func main() {
flag.Parse() flag.Parse()
if *debugFlag { if *debugFlag {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout}) log.SetDebugMode()
} }
if *versionFlag { if *versionFlag {
fmt.Printf("%s", version.FullVersion()) fmt.Printf("%s", version.FullVersion())
@ -36,7 +35,7 @@ func main() {
log.Fatal().Err(err).Msg("cmd/pomerium : failed to parse authenticator settings") log.Fatal().Err(err).Msg("cmd/pomerium : failed to parse authenticator settings")
} }
emailValidator := func(p *authenticate.Authenticator) error { emailValidator := func(p *authenticate.Authenticator) error {
p.Validator = options.NewEmailValidator(authOpts.EmailDomains) p.Validator = options.NewEmailValidator(authOpts.AllowedDomains)
return nil return nil
} }
@ -50,21 +49,13 @@ func main() {
log.Fatal().Err(err).Msg("cmd/pomerium : failed to parse proxy settings") log.Fatal().Err(err).Msg("cmd/pomerium : failed to parse proxy settings")
} }
validator := func(p *proxy.Proxy) error { p, err := proxy.NewProxy(proxyOpts)
p.EmailValidator = options.NewEmailValidator(proxyOpts.EmailDomains)
return nil
}
p, err := proxy.NewProxy(proxyOpts, validator)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium : failed to create proxy") log.Fatal().Err(err).Msg("cmd/pomerium : failed to create proxy")
} }
// proxyHandler := log.NewLoggingHandler(p.Handler())
authHandler := http.TimeoutHandler(authenticator.Handler(), authOpts.RequestTimeout, "")
topMux := http.NewServeMux() topMux := http.NewServeMux()
topMux.Handle(authOpts.Host+"/", authHandler) topMux.Handle(authOpts.RedirectURL.Host+"/", authenticator.Handler())
topMux.Handle("/", p.Handler()) topMux.Handle("/", p.Handler())
log.Fatal().Err(https.ListenAndServeTLS(nil, topMux)) log.Fatal().Err(https.ListenAndServeTLS(nil, topMux))

27
docs/.vuepress/config.js Normal file
View file

@ -0,0 +1,27 @@
// .vuepress/config.js
module.exports = {
title: "Pomerium",
description: "Just playing around",
themeConfig: {
repo: "pomerium/pomerium",
editLinks: true,
docsDir: "docs",
editLinkText: "Edit this page on GitHub",
lastUpdated: "Last Updated",
nav: [{text: "Guide", link: "/guide/"}],
sidebar: {
"/guide/": genSidebarConfig("Guide")
}
}
};
function genSidebarConfig(title) {
return [
{
title,
collapsable: false,
children: ["", "identity-providers"]
}
];
}

View file

@ -0,0 +1,4 @@
$accentColor = #6c63ff
$textColor = #2c3e50
$borderColor = #eaecef
$codeBgColor = #282c34

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -0,0 +1 @@
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" version="1.1" x="0px" y="0px"><title>Bilevel Viaduct</title><desc>Created with Sketch.</desc><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g fill="#000000"><path d="M68,69 L61,69 C61,62.9248678 56.0751322,58 50,58 C43.9248678,58 39,62.9248678 39,69 L31.955157,69 C31.9848374,68.670638 32,68.3370897 32,68 C32,61.9248678 27.0751322,57 21,57 C14.9248678,57 10,61.9248678 10,68 C10,68.3370897 10.0151626,68.670638 10.044843,69 L5,69 L5,49.5 L5,30 L95,30 L95,49.5 L95,69 L90,69 C90,62.9248678 85.0751322,58 79,58 C72.9248678,58 68,62.9248678 68,69 Z M10,49 L32,49 C32,42.9248678 27.0751322,38 21,38 C14.9248678,38 10,42.9248678 10,49 Z M39.044843,49 L60.955157,49 C60.4499282,43.3935 55.7380426,39 50,39 C44.2619574,39 39.5500718,43.3935 39.044843,49 Z M68.044843,49 L89.955157,49 C89.4499282,43.3935 84.7380426,39 79,39 C73.2619574,39 68.5500718,43.3935 68.044843,49 Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1,023 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View file

@ -0,0 +1,90 @@
---
title: Identity Providers
description: This article describes how to connect pomerium to third-party identity providers / single-sign-on services. You will need to generate keys, copy these into your promerium settings, and enable the connection.
---
# Identity Provider Configuration
This article describes how to configure pomerium to use a third-party identity service for single-sign-on.
There are a few configuration steps required for identity provider integration. Most providers support [OpenID Connect] which provides a standardized interface for authentication. In this guide we'll cover how to do the following for each identity provider:
1. Establish a **Redirect URL** with the identity provider which is called after authentication.
1. Generate a **Client ID** and **Client Secret**.
1. Configure pomerium to use the **Client ID** and **Client Secret** keys.
## Google
Log in to your Google account and go to the [APIs & services](https://console.developers.google.com/projectselector/apis/credentials). Navigate to **Credentials** using the left-hand menu.
![API Manager Credentials](./google/google-credentials.png)
On the **Credentials** page, click **Create credentials** and choose **OAuth Client ID**.
![Create New Credentials](./google/google-create-new-credentials.png)
On the **Create Client ID** page, select **Web application**. In the new fields that display, set the following parameters:
| Field | Description |
| ------------------------ | ----------------------------------------- |
| Name | The name of your web app |
| Authorized redirect URIs | `https://${redirect-url}/oauth2/callback` |
![Web App Credentials Configuration](./google/google-create-client-id-config.png)
Click **Create** to proceed.
Your `Client ID` and `Client Secret` will be displayed:
![OAuth Client ID and Secret](./google/google-oauth-client-info.png)
Set `Client ID` and `Client Secret` in Pomerium's settings. Your [environmental variables] should look something like this.
```bash
export REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback"
export IDP_PROVIDER="google"
export IDP_PROVIDER_URL="https://accounts.google.com"
export IDP_CLIENT_ID="yyyy.apps.googleusercontent.com"
export IDP_CLIENT_SECRET="xxxxxx"
```
## Okta
[Log in to your Okta account](https://login.okta.com) and head to your Okta dashboard. Select **Applications** on the top menu. On the Applications page, click the **Add Application** button to create a new app.
![Okta Applications Dashboard](./okta/okta-app-dashboard.png)
On the **Create New Application** page, select the **Web** for your application.
![Okta Create Application Select Platform](./okta/okta-create-app-platform.png)
Next, provide the following information for your application settings:
| Field | Description |
| ---------------------------- | ----------------------------------------------------- |
| Name | The name of your application. |
| Base URIs (optional) | The domain(s) of your application. |
| Login redirect URIs | `https://${redirect-url}/oauth2/callback`. |
| Group assignments (optional) | The user groups that can sign in to this application. |
| Grant type allowed | **You must enable Refresh Token.** |
![Okta Create Application Settings](./okta/okta-create-app-settings.png)
Click **Done** to proceed. You'll be taken to the **General** page of your app.
Go to the **General** page of your app and scroll down to the **Client Credentials** section. This section contains the **Client ID** and **Client Secret** to be used in the next step.
![Okta Client ID and Secret](./okta/okta-client-id-and-secret.png)
At this point, you will configure the integration from the Pomerium side. Your [environmental variables] should look something like this.
```bash
export REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback"
export IDP_PROVIDER="okta"
export IDP_PROVIDER_URL="https://dev-108295-admin.oktapreview.com/"
export IDP_CLIENT_ID="0oairksnr0C0fEJ7l0h7"
export IDP_CLIENT_SECRET="xxxxxx"
```
[environmental variables]: https://en.wikipedia.org/wiki/Environment_variable
[oauth2]: https://oauth.net/2/
[openid connect]: https://en.wikipedia.org/wiki/OpenID_Connect

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

44
docs/guide/readme.md Normal file
View file

@ -0,0 +1,44 @@
# Quick start
1. [Download] pre-built binaries or build Pomerium from source.
1. Generate a wild-card certificate for a test domain like `corp.example.com`. For convenience, an included [script] can generate a free one using LetsEncrypt and [certbot].
Once complete, move the generated public and private keys (`cert.pem`/`privkey.pem`) next to the pomerium binary. Certificates can also be set as environmental variables or dynamically with a [KMS].
1. Next, set configure your [identity provider](./identity-providers.md) by generating an OAuth **Client ID** and **Client Secret** as well as setting a **Redirect URL** endpoint. The Redirect URL endpoint will be called by the identity provider following user authentication.
1. Pomerium is configured using [environmental variables]. A minimal configuration is as follows.
```bash
# file : env
# The URL that the identity provider will call back after authenticating the user
export REDIRECT_URL="https://sso-auth.corp.example.com/oauth2/callback"
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
export SHARED_SECRET=REPLACE_ME
export COOKIE_SECRET=REPLACE_ME
# Allow users with emails from the following domain post-fix (e.g. example.com)
export ALLOWED_DOMAINS=*
## Identity Provider Settings
export IDP_PROVIDER="google"
export IDP_PROVIDER_URL="https://accounts.google.com" # optional for google
export IDP_CLIENT_ID="YOU_GOT_THIS_FROM_STEP-3.apps.googleusercontent.com"
export IDP_CLIENT_SECRET="YOU_GOT_THIS_FROM_STEP-3"
# key/value list of simple routes.
export ROUTES='http.corp.example.com':'httpbin.org'
```
You can also view the [env.example] configuration file for a more comprehensive list of options.
1. For a first run, I suggest setting the debug flag which provides user friendly logging.
```bash
source ./env
./pomerium -debug
```
[download]: https://github.com/pomerium/pomerium/releases
[environmental variables]: https://12factor.net/config
[env.example]: https://github.com/pomerium/pomerium/blob/master/env.example
[kms]: https://en.wikipedia.org/wiki/Key_management
[certbot]: https://certbot.eff.org/docs/install.html
[script]: https://github.com/pomerium/pomerium/blob/master/scripts/generate_wildcard_cert.sh

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

8
docs/readme.md Normal file
View file

@ -0,0 +1,8 @@
---
home: true
heroImage: logo.svg
heroText: Pomerium
tagline: Identity-aware access proxy.
actionText: Read the docs
actionLink: /guide/
---

View file

@ -1,26 +1,28 @@
#!/bin/bash #!/bin/bash
export HOST="sso-auth.corp.beyondperimeter.com" # The URL that the identity provider will call back after authenticating the user
export REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback" export REDIRECT_URL="https://sso-auth.corp.example.com/oauth2/callback"
export PROXY_ROOT_DOMAIN=beyondperimeter.com # Allow users with emails from the following domain post-fix (e.g. example.com)
export PROXY_CLIENT_ID=WLgwUNIJW6DtsnAM2ck510znU2T3l+WufPg67e50oVM= export ALLOWED_DOMAINS=*
export PROXY_CLIENT_SECRET=gFB0qsSxxPqCtoNMuF7Q1VupJSNEq0BguxlUfT0PE+Y= # Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
export SHARED_SECRET=9wiTZq4qvmS/plYQyvzGKWPlH/UBy0DMYMA2x/zngrM=
# Generate 256 bitrandom key to encrypt the cookie `head -c32 /dev/urandom | base64`
export AUTH_CODE_SECRET=9wiTZq4qvmS/plYQyvzGKWPlH/UBy0DMYMA2x/zngrM=
export COOKIE_SECRET=uPGHo1ujND/k3B9V6yr52Gweq3RRYfFho98jxDG5Br8= export COOKIE_SECRET=uPGHo1ujND/k3B9V6yr52Gweq3RRYfFho98jxDG5Br8=
export COOKIE_SECURE=true
# Valid email domains # OKTA
export EMAIL_DOMAIN=* # export IDP_PROVIDER="okta
export SSO_EMAIL_DOMAIN=* # export IDP_CLIENT_ID="REPLACEME"
# export IDP_CLIENT_SECRET="REPLACEME"
# export IDP_PROVIDER_URL="https://REPLACEME.oktapreview.com/oauth2/default"
# IdP configuration ## GOOGLE
export IDP_PROVIDER="google" export IDP_PROVIDER="google"
export IDP_PROVIDER_URL="https://sso-auth.corp.beyondperimeter.com" export IDP_PROVIDER_URL="https://accounts.google.com" # optional for google
export IDP_CLIENT_ID="xxx.apps.googleusercontent.com" export IDP_CLIENT_ID="REPLACE-ME.googleusercontent.com"
export IDP_CLIENT_SECRET="xxx" export IDP_CLIENT_SECRET="REPLACEME"
export IDP_REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback"
# export SCOPE="openid email" # generally, you want the default OIDC scopes
# k/v seperated list of simple routes.
export ROUTES='http.corp.example.com':'httpbin.org'
# proxy'd routes
export ROUTES='news.corp.beyondperimeter.com':'news.ycombinator.com','github.corp.beyondperimeter.com':'github.com'

View file

@ -13,6 +13,11 @@ import (
// Logger is the global logger. // Logger is the global logger.
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger() var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
// SetDebugMode tells the logger to use standard out and pretty print output.
func SetDebugMode() {
Logger = Logger.Output(zerolog.ConsoleWriter{Out: os.Stdout})
}
// Output duplicates the global logger and sets w as its output. // Output duplicates the global logger and sets w as its output.
func Output(w io.Writer) zerolog.Logger { func Output(w io.Writer) zerolog.Logger {
return Logger.Output(w) return Logger.Output(w)
@ -32,7 +37,7 @@ func WithRequest(req *http.Request, function string) zerolog.Logger {
Str("req-http-method", req.Method). Str("req-http-method", req.Method).
Str("req-host", req.Host). Str("req-host", req.Host).
Str("req-url", req.URL.String()). Str("req-url", req.URL.String()).
Str("req-user-agent", req.Header.Get("User-Agent")). // Str("req-user-agent", req.Header.Get("User-Agent")).
Logger() Logger()
} }

View file

@ -106,7 +106,7 @@ func logRequest(proxyHost, username string, req *http.Request, url url.URL, requ
Str("request-method", req.Method). Str("request-method", req.Method).
Str("request-uri", uri). Str("request-uri", uri).
Str("proxy-host", proxyHost). Str("proxy-host", proxyHost).
Str("user-agent", req.Header.Get("User-Agent")). // Str("user-agent", req.Header.Get("User-Agent")).
Str("remote-address", getRemoteAddr(req)). Str("remote-address", getRemoteAddr(req)).
Dur("duration", requestDuration). Dur("duration", requestDuration).
Str("user", username). Str("user", username).

View file

@ -40,47 +40,26 @@ func WithMethods(f http.HandlerFunc, methods ...string) http.HandlerFunc {
} }
} }
// ValidateClientID checks the request body or url for the client id and returns an error
// if it does not match the proxy client id
func ValidateClientID(f http.HandlerFunc, proxyClientID string) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
// try to get the client id from the request body
err := req.ParseForm()
if err != nil {
httputil.ErrorResponse(rw, req, err.Error(), http.StatusInternalServerError)
return
}
clientID := req.FormValue("client_id")
if clientID == "" {
// try to get the clientID from the query parameters
clientID = req.URL.Query().Get("client_id")
}
if clientID != proxyClientID {
httputil.ErrorResponse(rw, req, "Invalid client_id parameter", http.StatusUnauthorized)
return
}
f(rw, req)
}
}
// ValidateClientSecret checks the request header for the client secret and returns // ValidateClientSecret checks the request header for the client secret and returns
// an error if it does not match the proxy client secret // an error if it does not match the proxy client secret
func ValidateClientSecret(f http.HandlerFunc, proxyClientSecret string) http.HandlerFunc { func ValidateClientSecret(f http.HandlerFunc, sharedSecret string) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) { return func(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm() err := req.ParseForm()
if err != nil { if err != nil {
httputil.ErrorResponse(rw, req, err.Error(), http.StatusInternalServerError) httputil.ErrorResponse(rw, req, err.Error(), http.StatusInternalServerError)
return return
} }
clientSecret := req.Form.Get("client_secret") clientSecret := req.Form.Get("shared_secret")
// check the request header for the client secret // check the request header for the client secret
if clientSecret == "" { if clientSecret == "" {
clientSecret = req.Header.Get("X-Client-Secret") clientSecret = req.Header.Get("X-Client-Secret")
} }
if clientSecret != proxyClientSecret {
log.Error().Str("clientSecret", clientSecret).Str("proxyClientSecret", proxyClientSecret).Msg("oh") if clientSecret != sharedSecret {
log.Error().
Str("clientSecret", clientSecret).
Str("sharedSecret", sharedSecret).
Msg("middleware.ValidateClientSecret")
httputil.ErrorResponse(rw, req, "Invalid client secret", http.StatusUnauthorized) httputil.ErrorResponse(rw, req, "Invalid client secret", http.StatusUnauthorized)
return return
} }
@ -122,7 +101,7 @@ func validRedirectURI(uri string, rootDomains []string) bool {
// ValidateSignature ensures the request is valid and has been signed with // ValidateSignature ensures the request is valid and has been signed with
// the correspdoning client secret key // the correspdoning client secret key
func ValidateSignature(f http.HandlerFunc, proxyClientSecret string) http.HandlerFunc { func ValidateSignature(f http.HandlerFunc, sharedSecret string) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) { return func(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm() err := req.ParseForm()
if err != nil { if err != nil {
@ -132,7 +111,7 @@ func ValidateSignature(f http.HandlerFunc, proxyClientSecret string) http.Handle
redirectURI := req.Form.Get("redirect_uri") redirectURI := req.Form.Get("redirect_uri")
sigVal := req.Form.Get("sig") sigVal := req.Form.Get("sig")
timestamp := req.Form.Get("ts") timestamp := req.Form.Get("ts")
if !validSignature(redirectURI, sigVal, timestamp, proxyClientSecret) { if !validSignature(redirectURI, sigVal, timestamp, sharedSecret) {
httputil.ErrorResponse(rw, req, "Invalid redirect parameter", http.StatusBadRequest) httputil.ErrorResponse(rw, req, "Invalid redirect parameter", http.StatusBadRequest)
return return
} }
@ -154,6 +133,7 @@ func ValidateHost(h http.Handler, mux map[string]*http.Handler) http.Handler {
// RequireHTTPS reroutes a HTTP request to HTTPS // RequireHTTPS reroutes a HTTP request to HTTPS
// todo(bdd) : this is unreliable unless behind another reverser proxy // todo(bdd) : this is unreliable unless behind another reverser proxy
// todo(bdd) : header age seems extreme
func RequireHTTPS(h http.Handler) http.Handler { func RequireHTTPS(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Strict-Transport-Security", "max-age=31536000") rw.Header().Set("Strict-Transport-Security", "max-age=31536000")

View file

@ -25,7 +25,6 @@ type SessionState struct {
Email string `json:"email"` Email string `json:"email"`
User string `json:"user"` User string `json:"user"`
Groups []string `json:"groups"`
} }
// LifetimePeriodExpired returns true if the lifetime has expired // LifetimePeriodExpired returns true if the lifetime has expired

View file

@ -97,16 +97,16 @@ footer {
t = template.Must(t.Parse(` t = template.Must(t.Parse(`
{{define "sign_in_message.html"}} {{define "sign_in_message.html"}}
{{if eq (len .EmailDomains) 1}} {{if eq (len .AllowedDomains) 1}}
{{if eq (index .EmailDomains 0) "@*"}} {{if eq (index .AllowedDomains 0) "@*"}}
<p>You may sign in with any {{.ProviderName}} account.</p> <p>You may sign in with any {{.ProviderName}} account.</p>
{{else}} {{else}}
<p>You may sign in with your <b>{{index .EmailDomains 0}}</b> {{.ProviderName}} account.</p> <p>You may sign in with your <b>{{index .AllowedDomains 0}}</b> {{.ProviderName}} account.</p>
{{end}} {{end}}
{{else if gt (len .EmailDomains) 1}} {{else if gt (len .AllowedDomains) 1}}
<p> <p>
You may sign in with any of these {{.ProviderName}} accounts:<br> You may sign in with any of these {{.ProviderName}} accounts:<br>
{{range $i, $e := .EmailDomains}}{{if $i}}, {{end}}<b>{{$e}}</b>{{end}} {{range $i, $e := .AllowedDomains}}{{if $i}}, {{end}}<b>{{$e}}</b>{{end}}
</p> </p>
{{end}} {{end}}
{{end}}`)) {{end}}`))

6
package.json Normal file
View file

@ -0,0 +1,6 @@
{
"scripts": {
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
}
}

View file

@ -42,9 +42,8 @@ var (
type AuthenticateClient struct { type AuthenticateClient struct {
AuthenticateServiceURL *url.URL AuthenticateServiceURL *url.URL
// SharedKey string
ClientID string
ClientSecret string
SignInURL *url.URL SignInURL *url.URL
SignOutURL *url.URL SignOutURL *url.URL
RedeemURL *url.URL RedeemURL *url.URL
@ -58,12 +57,12 @@ type AuthenticateClient struct {
} }
// NewAuthenticateClient instantiates a new AuthenticateClient with provider data // NewAuthenticateClient instantiates a new AuthenticateClient with provider data
func NewAuthenticateClient(uri *url.URL, clientID, clientSecret string, sessionValid, sessionLifetime, gracePeriod time.Duration) *AuthenticateClient { func NewAuthenticateClient(uri *url.URL, sharedKey string, sessionValid, sessionLifetime, gracePeriod time.Duration) *AuthenticateClient {
return &AuthenticateClient{ return &AuthenticateClient{
AuthenticateServiceURL: uri, AuthenticateServiceURL: uri,
ClientID: clientID, // ClientID: clientID,
ClientSecret: clientSecret, SharedKey: sharedKey,
SignInURL: uri.ResolveReference(&url.URL{Path: "/sign_in"}), SignInURL: uri.ResolveReference(&url.URL{Path: "/sign_in"}),
SignOutURL: uri.ResolveReference(&url.URL{Path: "/sign_out"}), SignOutURL: uri.ResolveReference(&url.URL{Path: "/sign_out"}),
@ -112,9 +111,7 @@ func (p *AuthenticateClient) Redeem(redirectURL, code string) (*sessions.Session
} }
params := url.Values{} params := url.Values{}
// params that are validates by the authenticate service middleware params.Add("shared_secret", p.SharedKey)
params.Add("client_id", p.ClientID)
params.Add("client_secret", p.ClientSecret)
params.Add("code", code) params.Add("code", code)
req, err := p.newRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) req, err := p.newRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
@ -195,8 +192,7 @@ func (p *AuthenticateClient) RefreshSession(s *sessions.SessionState) (bool, err
func (p *AuthenticateClient) redeemRefreshToken(refreshToken string) (token string, expires time.Duration, err error) { func (p *AuthenticateClient) redeemRefreshToken(refreshToken string) (token string, expires time.Duration, err error) {
params := url.Values{} params := url.Values{}
params.Add("client_id", p.ClientID) params.Add("shared_secret", p.SharedKey)
params.Add("client_secret", p.ClientSecret)
params.Add("refresh_token", refreshToken) params.Add("refresh_token", refreshToken)
var req *http.Request var req *http.Request
req, err = p.newRequest("POST", p.RefreshURL.String(), bytes.NewBufferString(params.Encode())) req, err = p.newRequest("POST", p.RefreshURL.String(), bytes.NewBufferString(params.Encode()))
@ -241,13 +237,13 @@ func (p *AuthenticateClient) redeemRefreshToken(refreshToken string) (token stri
func (p *AuthenticateClient) ValidateSessionState(s *sessions.SessionState) bool { func (p *AuthenticateClient) ValidateSessionState(s *sessions.SessionState) bool {
// we validate the user's access token is valid // we validate the user's access token is valid
params := url.Values{} params := url.Values{}
params.Add("client_id", p.ClientID) 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.Error().Err(err).Str("user", s.Email).Msg("proxy/authenticator.ValidateSessionState : error validating session state")
return false return false
} }
req.Header.Set("X-Client-Secret", p.ClientSecret) req.Header.Set("X-Client-Secret", p.SharedKey)
req.Header.Set("X-Access-Token", s.AccessToken) req.Header.Set("X-Access-Token", s.AccessToken)
req.Header.Set("X-Id-Token", s.IDToken) req.Header.Set("X-Id-Token", s.IDToken)
@ -281,7 +277,7 @@ func (p *AuthenticateClient) ValidateSessionState(s *sessions.SessionState) bool
// signRedirectURL signs the redirect url string, given a timestamp, and returns it // signRedirectURL signs the redirect url string, given a timestamp, and returns it
func (p *AuthenticateClient) signRedirectURL(rawRedirect string, timestamp time.Time) string { func (p *AuthenticateClient) signRedirectURL(rawRedirect string, timestamp time.Time) string {
h := hmac.New(sha256.New, []byte(p.ClientSecret)) h := hmac.New(sha256.New, []byte(p.SharedKey))
h.Write([]byte(rawRedirect)) h.Write([]byte(rawRedirect))
h.Write([]byte(fmt.Sprint(timestamp.Unix()))) h.Write([]byte(fmt.Sprint(timestamp.Unix())))
return base64.URLEncoding.EncodeToString(h.Sum(nil)) return base64.URLEncoding.EncodeToString(h.Sum(nil))
@ -294,7 +290,7 @@ func (p *AuthenticateClient) GetSignInURL(redirectURL *url.URL, state string) *u
rawRedirect := redirectURL.String() rawRedirect := redirectURL.String()
params, _ := url.ParseQuery(a.RawQuery) params, _ := url.ParseQuery(a.RawQuery)
params.Set("redirect_uri", rawRedirect) params.Set("redirect_uri", rawRedirect)
params.Set("client_id", p.ClientID) params.Set("shared_secret", p.SharedKey)
params.Set("response_type", "code") params.Set("response_type", "code")
params.Add("state", state) params.Add("state", state)
params.Set("ts", fmt.Sprint(now.Unix())) params.Set("ts", fmt.Sprint(now.Unix()))

View file

@ -7,7 +7,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"reflect" "reflect"
"strings"
"github.com/pomerium/pomerium/internal/aead" "github.com/pomerium/pomerium/internal/aead"
"github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/httputil"
@ -90,15 +89,8 @@ func (p *Proxy) PingPage(rw http.ResponseWriter, _ *http.Request) {
func (p *Proxy) SignOut(rw http.ResponseWriter, req *http.Request) { func (p *Proxy) SignOut(rw http.ResponseWriter, req *http.Request) {
p.sessionStore.ClearSession(rw, req) p.sessionStore.ClearSession(rw, req)
var scheme string
// Build redirect URI from request host
if req.URL.Scheme == "" {
scheme = "https"
}
redirectURL := &url.URL{ redirectURL := &url.URL{
Scheme: scheme, Scheme: "https",
Host: req.Host, Host: req.Host,
Path: "/", Path: "/",
} }
@ -185,7 +177,7 @@ func (p *Proxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
// we encrypt this value to be opaque the browser cookie // we encrypt this value to be opaque the browser cookie
// this value will be unique since we always use a randomized nonce as part of marshaling // this value will be unique since we always use a randomized nonce as part of marshaling
encryptedCSRF, err := p.CookieCipher.Marshal(state) encryptedCSRF, err := p.cipher.Marshal(state)
if err != nil { if err != nil {
requestLog.Error().Err(err).Msg("failed to marshal csrf") requestLog.Error().Err(err).Msg("failed to marshal csrf")
p.ErrorPage(rw, req, http.StatusInternalServerError, "Internal Error", err.Error()) p.ErrorPage(rw, req, http.StatusInternalServerError, "Internal Error", err.Error())
@ -195,7 +187,7 @@ func (p *Proxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
// we encrypt this value to be opaque the uri query value // we encrypt this value to be opaque the uri query value
// this value will be unique since we always use a randomized nonce as part of marshaling // this value will be unique since we always use a randomized nonce as part of marshaling
encryptedState, err := p.CookieCipher.Marshal(state) encryptedState, err := p.cipher.Marshal(state)
if err != nil { if err != nil {
requestLog.Error().Err(err).Msg("failed to encrypt cookie") requestLog.Error().Err(err).Msg("failed to encrypt cookie")
p.ErrorPage(rw, req, http.StatusInternalServerError, "Internal Error", err.Error()) p.ErrorPage(rw, req, http.StatusInternalServerError, "Internal Error", err.Error())
@ -238,14 +230,14 @@ func (p *Proxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
encryptedState := req.Form.Get("state") encryptedState := req.Form.Get("state")
stateParameter := &StateParameter{} stateParameter := &StateParameter{}
err = p.CookieCipher.Unmarshal(encryptedState, stateParameter) err = p.cipher.Unmarshal(encryptedState, stateParameter)
if err != nil { if err != nil {
requestLog.Error().Err(err).Msg("could not unmarshal state") requestLog.Error().Err(err).Msg("could not unmarshal state")
p.ErrorPage(rw, req, http.StatusInternalServerError, "Internal Error", "Internal Error") p.ErrorPage(rw, req, http.StatusInternalServerError, "Internal Error", "Internal Error")
return return
} }
c, err := req.Cookie(p.CSRFCookieName) c, err := p.csrfStore.GetCSRF(req)
if err != nil { if err != nil {
requestLog.Error().Err(err).Msg("failed parsing csrf cookie") requestLog.Error().Err(err).Msg("failed parsing csrf cookie")
p.ErrorPage(rw, req, http.StatusBadRequest, "Bad Request", err.Error()) p.ErrorPage(rw, req, http.StatusBadRequest, "Bad Request", err.Error())
@ -255,7 +247,7 @@ func (p *Proxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
encryptedCSRF := c.Value encryptedCSRF := c.Value
csrfParameter := &StateParameter{} csrfParameter := &StateParameter{}
err = p.CookieCipher.Unmarshal(encryptedCSRF, csrfParameter) err = p.cipher.Unmarshal(encryptedCSRF, csrfParameter)
if err != nil { if err != nil {
requestLog.Error().Err(err).Msg("couldn't unmarshal CSRF") requestLog.Error().Err(err).Msg("couldn't unmarshal CSRF")
p.ErrorPage(rw, req, http.StatusInternalServerError, "Internal Error", "Internal Error") p.ErrorPage(rw, req, http.StatusInternalServerError, "Internal Error", "Internal Error")
@ -274,16 +266,6 @@ func (p *Proxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
return return
} }
// We validate the user information, and check that this user has proper authorization
// for the resources requested. This can be set via the email address or any groups.
//
// set cookie, or deny
if !p.EmailValidator(session.Email) {
requestLog.Error().Str("user", session.Email).Msg("permission denied: unauthorized")
p.ErrorPage(rw, req, http.StatusForbidden, "Permission Denied", "Invalid Account")
return
}
// We store the session in a cookie and redirect the user back to the application // We store the session in a cookie and redirect the user back to the application
err = p.sessionStore.SaveSession(rw, req, session) err = p.sessionStore.SaveSession(rw, req, session)
if err != nil { if err != nil {
@ -351,7 +333,6 @@ func (p *Proxy) Proxy(rw http.ResponseWriter, req *http.Request) {
return return
} }
// overhead := time.Now().Sub(start)
route.ServeHTTP(rw, req) route.ServeHTTP(rw, req)
} }
@ -432,10 +413,12 @@ func (p *Proxy) Authenticate(rw http.ResponseWriter, req *http.Request) (err err
} }
} }
if !p.EmailValidator(session.Email) { // if !p.EmailValidator(session.Email) {
requestLog.Error().Str("user", session.Email).Msg("email failed to validate, unauthorized") // requestLog.Error().Str("user", session.Email).Msg("email failed to validate, unauthorized")
return ErrUserNotAuthorized // return ErrUserNotAuthorized
} // }
//
// todo(bdd) : handled by authorize package
req.Header.Set("X-Forwarded-User", session.User) req.Header.Set("X-Forwarded-User", session.User)
@ -444,7 +427,6 @@ func (p *Proxy) Authenticate(rw http.ResponseWriter, req *http.Request) (err err
} }
req.Header.Set("X-Forwarded-Email", session.Email) req.Header.Set("X-Forwarded-Email", session.Email)
req.Header.Set("X-Forwarded-Groups", strings.Join(session.Groups, ","))
// stash authenticated user so that it can be logged later (see func logRequest) // stash authenticated user so that it can be logged later (see func logRequest)
rw.Header().Set(loggingUserHeader, session.Email) rw.Header().Set(loggingUserHeader, session.Email)

View file

@ -23,14 +23,10 @@ import (
// Options represents the configuration options for the proxy service. // Options represents the configuration options for the proxy service.
type Options struct { type Options struct {
// AuthenticateServiceURL specifies the url to the pomerium authenticate http service. // AuthenticateServiceURL specifies the url to the pomerium authenticate http service.
AuthenticateServiceURL *url.URL `envconfig:"PROVIDER_URL"` AuthenticateServiceURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
// EmailDomains is a string slice of valid domains to proxy // todo(bdd) : replace with certificate based mTLS
EmailDomains []string `envconfig:"EMAIL_DOMAIN"` SharedKey string `envconfig:"SHARED_SECRET"`
// todo(bdd): ClientID and ClientSecret are used are a hacky pre shared key
// prefer certificates and mutual tls
ClientID string `envconfig:"PROXY_CLIENT_ID"`
ClientSecret string `envconfig:"PROXY_CLIENT_SECRET"`
DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"` DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"`
@ -38,7 +34,6 @@ type Options struct {
CookieSecret string `envconfig:"COOKIE_SECRET"` CookieSecret string `envconfig:"COOKIE_SECRET"`
CookieDomain string `envconfig:"COOKIE_DOMAIN"` CookieDomain string `envconfig:"COOKIE_DOMAIN"`
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"` CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
CookieSecure bool `envconfig:"COOKIE_SECURE" `
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"` CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
PassAccessToken bool `envconfig:"PASS_ACCESS_TOKEN"` PassAccessToken bool `envconfig:"PASS_ACCESS_TOKEN"`
@ -54,12 +49,11 @@ type Options struct {
// NewOptions returns a new options struct // NewOptions returns a new options struct
var defaultOptions = &Options{ var defaultOptions = &Options{
CookieName: "_pomerium_proxy", CookieName: "_pomerium_proxy",
CookieSecure: true, CookieHTTPOnly: false,
CookieHTTPOnly: true,
CookieExpire: time.Duration(168) * time.Hour, CookieExpire: time.Duration(168) * time.Hour,
DefaultUpstreamTimeout: time.Duration(10) * time.Second, DefaultUpstreamTimeout: time.Duration(10) * time.Second,
SessionLifetimeTTL: time.Duration(720) * time.Hour, SessionLifetimeTTL: time.Duration(720) * time.Hour,
SessionValidTTL: time.Duration(1) * time.Minute, SessionValidTTL: time.Duration(10) * time.Minute,
GracePeriodTTL: time.Duration(3) * time.Hour, GracePeriodTTL: time.Duration(3) * time.Hour,
PassAccessToken: false, PassAccessToken: false,
} }
@ -91,22 +85,20 @@ func (o *Options) Validate() error {
if o.AuthenticateServiceURL == nil { if o.AuthenticateServiceURL == nil {
return errors.New("missing setting: provider-url") return errors.New("missing setting: provider-url")
} }
if o.AuthenticateServiceURL.Scheme != "https" {
return errors.New("provider-url must be a valid https url")
}
if o.CookieSecret == "" { if o.CookieSecret == "" {
return errors.New("missing setting: cookie-secret") return errors.New("missing setting: cookie-secret")
} }
if o.ClientID == "" {
return errors.New("missing setting: client-id") if o.SharedKey == "" {
}
if o.ClientSecret == "" {
return errors.New("missing setting: client-secret") return errors.New("missing setting: client-secret")
} }
if len(o.EmailDomains) == 0 {
return errors.New("missing setting: email-domain")
}
decodedCookieSecret, err := base64.StdEncoding.DecodeString(o.CookieSecret) decodedCookieSecret, err := base64.StdEncoding.DecodeString(o.CookieSecret)
if err != nil { if err != nil {
return errors.New("cookie secret is invalid (e.g. `head -c33 /dev/urandom | base64`) ") return errors.New("cookie secret is invalid (e.g. `head -c32 /dev/urandom | base64`) ")
} }
validCookieSecretLength := false validCookieSecretLength := false
for _, i := range []int{32, 64} { for _, i := range []int{32, 64} {
@ -122,24 +114,14 @@ func (o *Options) Validate() error {
// Proxy stores all the information associated with proxying a request. // Proxy stores all the information associated with proxying a request.
type Proxy struct { type Proxy struct {
CookieCipher aead.Cipher
CookieDomain string
CookieExpire time.Duration
CookieHTTPOnly bool
CookieName string
CookieSecure bool
CookieSeed string
CSRFCookieName string
EmailValidator func(string) bool
PassAccessToken bool PassAccessToken bool
// services // services
authenticateClient *authenticator.AuthenticateClient authenticateClient *authenticator.AuthenticateClient
// session // session
cipher aead.Cipher
csrfStore sessions.CSRFStore csrfStore sessions.CSRFStore
sessionStore sessions.SessionStore sessionStore sessions.SessionStore
cipher aead.Cipher
redirectURL *url.URL // the url to receive requests at redirectURL *url.URL // the url to receive requests at
templates *template.Template templates *template.Template
@ -154,13 +136,14 @@ type StateParameter struct {
// NewProxy takes a Proxy service from options and a validation function. // NewProxy takes a Proxy service from options and a validation function.
// Function returns an error if options fail to validate. // Function returns an error if options fail to validate.
func NewProxy(opts *Options, optFuncs ...func(*Proxy) error) (*Proxy, error) { func NewProxy(opts *Options) (*Proxy, error) {
if opts == nil { if opts == nil {
return nil, errors.New("options cannot be nil") return nil, errors.New("options cannot be nil")
} }
if err := opts.Validate(); err != nil { if err := opts.Validate(); err != nil {
return nil, err return nil, err
} }
// error explicitly handled by validate // error explicitly handled by validate
decodedSecret, _ := base64.StdEncoding.DecodeString(opts.CookieSecret) decodedSecret, _ := base64.StdEncoding.DecodeString(opts.CookieSecret)
cipher, err := aead.NewMiscreantCipher(decodedSecret) cipher, err := aead.NewMiscreantCipher(decodedSecret)
@ -174,7 +157,6 @@ func NewProxy(opts *Options, optFuncs ...func(*Proxy) error) (*Proxy, error) {
c.CookieDomain = opts.CookieDomain c.CookieDomain = opts.CookieDomain
c.CookieHTTPOnly = opts.CookieHTTPOnly c.CookieHTTPOnly = opts.CookieHTTPOnly
c.CookieExpire = opts.CookieExpire c.CookieExpire = opts.CookieExpire
c.CookieSecure = opts.CookieSecure
return nil return nil
}) })
@ -184,9 +166,7 @@ func NewProxy(opts *Options, optFuncs ...func(*Proxy) error) (*Proxy, error) {
authClient := authenticator.NewAuthenticateClient( authClient := authenticator.NewAuthenticateClient(
opts.AuthenticateServiceURL, opts.AuthenticateServiceURL,
// todo(bdd): fields below can be dropped as Client data provides redudent auth opts.SharedKey,
opts.ClientID,
opts.ClientSecret,
// todo(bdd): fields below should be passed as function args // todo(bdd): fields below should be passed as function args
opts.SessionLifetimeTTL, opts.SessionLifetimeTTL,
opts.SessionValidTTL, opts.SessionValidTTL,
@ -194,21 +174,12 @@ func NewProxy(opts *Options, optFuncs ...func(*Proxy) error) (*Proxy, error) {
) )
p := &Proxy{ p := &Proxy{
CookieCipher: cipher,
CookieDomain: opts.CookieDomain,
CookieExpire: opts.CookieExpire,
CookieHTTPOnly: opts.CookieHTTPOnly,
CookieName: opts.CookieName,
CookieSecure: opts.CookieSecure,
CookieSeed: string(decodedSecret),
CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"),
// these fields make up the routing mechanism // these fields make up the routing mechanism
mux: make(map[string]*http.Handler), mux: make(map[string]*http.Handler),
// session state // session state
cipher: cipher,
csrfStore: cookieStore, csrfStore: cookieStore,
sessionStore: cookieStore, sessionStore: cookieStore,
cipher: cipher,
authenticateClient: authClient, authenticateClient: authClient,
redirectURL: &url.URL{Path: "/.pomerium/callback"}, redirectURL: &url.URL{Path: "/.pomerium/callback"},
@ -216,13 +187,6 @@ func NewProxy(opts *Options, optFuncs ...func(*Proxy) error) (*Proxy, error) {
PassAccessToken: opts.PassAccessToken, PassAccessToken: opts.PassAccessToken,
} }
for _, optFunc := range optFuncs {
err := optFunc(p)
if err != nil {
return nil, err
}
}
for from, to := range opts.Routes { for from, to := range opts.Routes {
fromURL, _ := urlParse(from) fromURL, _ := urlParse(from)
toURL, _ := urlParse(to) toURL, _ := urlParse(to)
@ -232,16 +196,6 @@ func NewProxy(opts *Options, optFuncs ...func(*Proxy) error) (*Proxy, error) {
log.Info().Str("from", fromURL.Host).Str("to", toURL.String()).Msg("proxy.NewProxy : route") log.Info().Str("from", fromURL.Host).Str("to", toURL.String()).Msg("proxy.NewProxy : route")
} }
log.Info().
Str("CookieName", p.CookieName).
Str("redirectURL", p.redirectURL.String()).
Str("CSRFCookieName", p.CSRFCookieName).
Bool("CookieSecure", p.CookieSecure).
Str("CookieDomain", p.CookieDomain).
Bool("CookieHTTPOnly", p.CookieHTTPOnly).
Dur("CookieExpire", opts.CookieExpire).
Msg("proxy.NewProxy")
return p, nil return p, nil
} }
@ -261,7 +215,7 @@ var defaultUpstreamTransport = &http.Transport{
}).DialContext, }).DialContext,
MaxIdleConns: 100, MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 30 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
} }
@ -279,13 +233,8 @@ func deleteSSOCookieHeader(req *http.Request, cookieName string) {
// ServeHTTP signs the http request and deletes cookie headers // ServeHTTP signs the http request and deletes cookie headers
// before calling the upstream's ServeHTTP function. // before calling the upstream's ServeHTTP function.
func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
requestLog := log.WithRequest(r, "proxy.ServeHTTP")
deleteSSOCookieHeader(r, u.cookieName) deleteSSOCookieHeader(r, u.cookieName)
start := time.Now()
u.handler.ServeHTTP(w, r) u.handler.ServeHTTP(w, r)
duration := time.Since(start)
requestLog.Debug().Dur("duration", duration).Msg("proxy-request")
} }
// NewReverseProxy creates a reverse proxy to a specified url. // NewReverseProxy creates a reverse proxy to a specified url.
@ -295,17 +244,18 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func NewReverseProxy(to *url.URL) *httputil.ReverseProxy { func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
proxy := httputil.NewSingleHostReverseProxy(to) proxy := httputil.NewSingleHostReverseProxy(to)
proxy.Transport = defaultUpstreamTransport proxy.Transport = defaultUpstreamTransport
director := proxy.Director director := proxy.Director
proxy.Director = func(req *http.Request) { proxy.Director = func(req *http.Request) {
req.Header.Add("X-Forwarded-Host", req.Host) req.Header.Add("X-Forwarded-Host", req.Host)
director(req) director(req)
req.Host = to.Host req.Host = to.Host
} }
return proxy return proxy
} }
// NewReverseProxyHandler applies handler specific options to a given // NewReverseProxyHandler applies handler specific options to a given route.
// route.
func NewReverseProxyHandler(opts *Options, reverseProxy *httputil.ReverseProxy, serviceName string) http.Handler { func NewReverseProxyHandler(opts *Options, reverseProxy *httputil.ReverseProxy, serviceName string) http.Handler {
upstreamProxy := &UpstreamProxy{ upstreamProxy := &UpstreamProxy{
name: serviceName, name: serviceName,

View file

@ -20,7 +20,7 @@ func TestOptionsFromEnvConfig(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{"good default, no env settings", defaultOptions, "", "", false}, {"good default, no env settings", defaultOptions, "", "", false},
{"bad url", nil, "PROVIDER_URL", "%.rjlw", true}, {"bad url", nil, "AUTHENTICATE_SERVICE_URL", "%.rjlw", true},
{"good duration", defaultOptions, "SESSION_VALID_TTL", "1m", false}, {"good duration", defaultOptions, "SESSION_VALID_TTL", "1m", false},
{"bad duration", nil, "SESSION_VALID_TTL", "1sm", true}, {"bad duration", nil, "SESSION_VALID_TTL", "1sm", true},
} }
@ -127,9 +127,7 @@ func testOptions() *Options {
return &Options{ return &Options{
Routes: map[string]string{"corp.example.com": "example.com"}, Routes: map[string]string{"corp.example.com": "example.com"},
AuthenticateServiceURL: authurl, AuthenticateServiceURL: authurl,
ClientID: "yksYDhIM7PZTvdFP3Mi3sYt2JXooTi7y0oIClBR46fs=", SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
ClientSecret: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
EmailDomains: []string{"*"},
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=", CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
} }
} }
@ -147,14 +145,10 @@ func TestOptions_Validate(t *testing.T) {
invalidCookieSecret := testOptions() invalidCookieSecret := testOptions()
invalidCookieSecret.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw^" invalidCookieSecret.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw^"
shortCookieLength := testOptions() shortCookieLength := testOptions()
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg==" //head -c31 /dev/urandom | base64 shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
badClientID := testOptions() badSharedKey := testOptions()
badClientID.ClientID = "" badSharedKey.SharedKey = ""
badClientSecret := testOptions()
badClientSecret.ClientSecret = ""
badEmailDomain := testOptions()
badEmailDomain.EmailDomains = nil
tests := []struct { tests := []struct {
name string name string
@ -170,9 +164,7 @@ func TestOptions_Validate(t *testing.T) {
{"bad - no cookie secret", emptyCookieSecret, true}, {"bad - no cookie secret", emptyCookieSecret, true},
{"bad - invalid cookie secret", invalidCookieSecret, true}, {"bad - invalid cookie secret", invalidCookieSecret, true},
{"bad - short cookie secret", shortCookieLength, true}, {"bad - short cookie secret", shortCookieLength, true},
{"bad - no client id", badClientID, true}, {"bad - no shared secret", badSharedKey, true},
{"bad - no client secret", badClientSecret, true},
{"bad - no email domain", badEmailDomain, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -187,7 +179,7 @@ func TestOptions_Validate(t *testing.T) {
func TestNewProxy(t *testing.T) { func TestNewProxy(t *testing.T) {
good := testOptions() good := testOptions()
shortCookieLength := testOptions() shortCookieLength := testOptions()
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg==" //head -c31 /dev/urandom | base64 shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
tests := []struct { tests := []struct {
name string name string
@ -204,7 +196,7 @@ func TestNewProxy(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := NewProxy(tt.opts, tt.optFuncs...) got, err := NewProxy(tt.opts)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("NewProxy() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("NewProxy() error = %v, wantErr %v", err, tt.wantErr)
return return

View file

@ -0,0 +1,11 @@
#!/bin/bash
# requires certbot
certbot certonly --manual \
--agree-tos \
-d *.corp.example.com \
--preferred-challenges dns-01 \
--server https://acme-v02.api.letsencrypt.org/directory \
--config-dir le/config \
--logs-dir le/work \
--work-dir le/work