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.
40
.gitignore
vendored
|
@ -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/*
|
18
README.md
|
@ -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
|
|
||||||
[](https://travis-ci.org/pomerium/pomerium)
|
[](https://travis-ci.org/pomerium/pomerium)
|
||||||
[](https://goreportcard.com/report/github.com/pomerium/pomerium)
|
[](https://goreportcard.com/report/github.com/pomerium/pomerium)
|
||||||
[](https://github.com/pomerium/pomerium/blob/master/LICENSE)
|
[](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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
@ -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"]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
4
docs/.vuepress/override.styl
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
$accentColor = #6c63ff
|
||||||
|
$textColor = #2c3e50
|
||||||
|
$borderColor = #eaecef
|
||||||
|
$codeBgColor = #282c34
|
BIN
docs/.vuepress/public/getting-started.gif
Normal file
After Width: | Height: | Size: 1.8 MiB |
1
docs/.vuepress/public/logo.svg
Normal 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 |
BIN
docs/guide/google/google-create-client-id-config.png
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
docs/guide/google/google-create-new-credentials.png
Normal file
After Width: | Height: | Size: 99 KiB |
BIN
docs/guide/google/google-credentials.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
docs/guide/google/google-oauth-client-info.png
Normal file
After Width: | Height: | Size: 92 KiB |
90
docs/guide/identity-providers.md
Normal 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
On the **Credentials** page, click **Create credentials** and choose **OAuth Client ID**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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` |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Click **Create** to proceed.
|
||||||
|
|
||||||
|
Your `Client ID` and `Client Secret` will be displayed:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
On the **Create New Application** page, select the **Web** for your application.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.** |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
|

|
||||||
|
|
||||||
|
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
|
BIN
docs/guide/okta/okta-app-dashboard.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
docs/guide/okta/okta-client-id-and-secret.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
docs/guide/okta/okta-create-app-platform.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
docs/guide/okta/okta-create-app-settings.png
Normal file
After Width: | Height: | Size: 139 KiB |
44
docs/guide/readme.md
Normal 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
|
BIN
docs/logo.png
Before Width: | Height: | Size: 1.8 KiB |
8
docs/readme.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
home: true
|
||||||
|
heroImage: logo.svg
|
||||||
|
heroText: Pomerium
|
||||||
|
tagline: Identity-aware access proxy.
|
||||||
|
actionText: Read the docs
|
||||||
|
actionLink: /guide/
|
||||||
|
---
|
40
env.example
|
@ -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'
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"docs:dev": "vuepress dev docs",
|
||||||
|
"docs:build": "vuepress build docs"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
11
scripts/generate_wildcard_cert.sh
Executable 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
|