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
|
||||
website/vendor
|
||||
website/.bundle
|
||||
website/build
|
||||
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
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
dist/*
|
||||
|
||||
bin/*
|
||||
tags
|
||||
|
||||
# Editor backups
|
||||
|
@ -68,29 +59,22 @@ tags
|
|||
*.ipr
|
||||
*.iml
|
||||
|
||||
# compiled output
|
||||
ui/dist
|
||||
ui/tmp
|
||||
ui/root
|
||||
http/bindata_assetfs.go
|
||||
|
||||
# dependencies
|
||||
ui/node_modules
|
||||
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
|
||||
node_modules
|
||||
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://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.
|
||||
|
||||
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.
|
||||
- deploy mutually TLS (mTLS) encryption.
|
||||
- aggregate logging and telemetry data.
|
||||
|
||||
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.
|
||||
|
||||
## 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].
|
||||
|
||||
|
@ -32,4 +36,4 @@ $ ./bin/pomerium -debug
|
|||
```
|
||||
|
||||
[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"
|
||||
)
|
||||
|
||||
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
|
||||
type Options struct {
|
||||
// e.g.
|
||||
Host string `envconfig:"HOST"`
|
||||
//
|
||||
ProxyClientID string `envconfig:"PROXY_CLIENT_ID"`
|
||||
ProxyClientSecret string `envconfig:"PROXY_CLIENT_SECRET"`
|
||||
RedirectURL *url.URL `envconfig:"REDIRECT_URL" ` // e.g. auth.example.com/oauth/callback
|
||||
|
||||
SharedKey string `envconfig:"SHARED_SECRET"`
|
||||
|
||||
// Coarse authorization based on user email domain
|
||||
EmailDomains []string `envconfig:"SSO_EMAIL_DOMAIN"`
|
||||
AllowedDomains []string `envconfig:"ALLOWED_DOMAINS"`
|
||||
ProxyRootDomains []string `envconfig:"PROXY_ROOT_DOMAIN"`
|
||||
|
||||
// Session/Cookie management
|
||||
CookieName string
|
||||
CookieSecret string `envconfig:"COOKIE_SECRET"`
|
||||
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
|
||||
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE" default:"168h"`
|
||||
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH" default:"1h"`
|
||||
CookieSecure bool `envconfig:"COOKIE_SECURE" default:"true"`
|
||||
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY" default:"true"`
|
||||
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
|
||||
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
|
||||
CookieSecure bool `envconfig:"COOKIE_SECURE"`
|
||||
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
|
||||
|
||||
AuthCodeSecret string `envconfig:"AUTH_CODE_SECRET"`
|
||||
|
||||
SessionLifetimeTTL time.Duration `envconfig:"SESSION_LIFETIME_TTL" default:"720h"`
|
||||
SessionLifetimeTTL time.Duration `envconfig:"SESSION_LIFETIME_TTL"`
|
||||
|
||||
// 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
|
||||
ClientSecret string `envconfig:"IDP_CLIENT_SECRET"` // IdP Secret
|
||||
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"`
|
||||
|
||||
// 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
|
||||
|
@ -84,9 +69,7 @@ func OptionsFromEnvConfig() (*Options, error) {
|
|||
// The checks do not modify the internal state of the Option structure. Function returns
|
||||
// on first error found.
|
||||
func (o *Options) Validate() error {
|
||||
if o.ProviderURL == nil {
|
||||
return errors.New("missing setting: identity provider url")
|
||||
}
|
||||
|
||||
if o.RedirectURL == nil {
|
||||
return errors.New("missing setting: identity provider redirect url")
|
||||
}
|
||||
|
@ -100,17 +83,14 @@ func (o *Options) Validate() error {
|
|||
if o.ClientSecret == "" {
|
||||
return errors.New("missing setting: client secret")
|
||||
}
|
||||
if len(o.EmailDomains) == 0 {
|
||||
if len(o.AllowedDomains) == 0 {
|
||||
return errors.New("missing setting email domain")
|
||||
}
|
||||
if len(o.ProxyRootDomains) == 0 {
|
||||
return errors.New("missing setting: proxy root domain")
|
||||
}
|
||||
if o.ProxyClientID == "" {
|
||||
return errors.New("missing setting: proxy client id")
|
||||
}
|
||||
if o.ProxyClientSecret == "" {
|
||||
return errors.New("missing setting: proxy client secret")
|
||||
if o.SharedKey == "" {
|
||||
return errors.New("missing setting: shared secret")
|
||||
}
|
||||
|
||||
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.
|
||||
type Authenticator struct {
|
||||
RedirectURL *url.URL
|
||||
|
||||
Validator func(string) bool
|
||||
|
||||
EmailDomains []string
|
||||
AllowedDomains []string
|
||||
ProxyRootDomains []string
|
||||
Host string
|
||||
CookieSecure bool
|
||||
|
||||
ProxyClientID string
|
||||
ProxyClientSecret string
|
||||
SharedKey string
|
||||
|
||||
SessionLifetimeTTL time.Duration
|
||||
|
||||
|
@ -159,7 +139,6 @@ type Authenticator struct {
|
|||
sessionStore sessions.SessionStore
|
||||
cipher aead.Cipher
|
||||
|
||||
redirectURL *url.URL
|
||||
provider providers.Provider
|
||||
}
|
||||
|
||||
|
@ -171,7 +150,7 @@ func NewAuthenticator(opts *Options, optionFuncs ...func(*Authenticator) error)
|
|||
if err := opts.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decodedAuthCodeSecret, err := base64.StdEncoding.DecodeString(opts.AuthCodeSecret)
|
||||
decodedAuthCodeSecret, err := base64.StdEncoding.DecodeString(opts.CookieSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -198,12 +177,11 @@ func NewAuthenticator(opts *Options, optionFuncs ...func(*Authenticator) error)
|
|||
}
|
||||
|
||||
p := &Authenticator{
|
||||
ProxyClientID: opts.ProxyClientID,
|
||||
ProxyClientSecret: opts.ProxyClientSecret,
|
||||
EmailDomains: opts.EmailDomains,
|
||||
SharedKey: opts.SharedKey,
|
||||
AllowedDomains: opts.AllowedDomains,
|
||||
ProxyRootDomains: dotPrependDomains(opts.ProxyRootDomains),
|
||||
CookieSecure: opts.CookieSecure,
|
||||
redirectURL: opts.RedirectURL,
|
||||
RedirectURL: opts.RedirectURL,
|
||||
templates: templates.New(),
|
||||
csrfStore: cookieStore,
|
||||
sessionStore: cookieStore,
|
||||
|
@ -229,11 +207,10 @@ func newProvider(opts *Options) (providers.Provider, error) {
|
|||
pd := &providers.ProviderData{
|
||||
RedirectURL: opts.RedirectURL,
|
||||
ProviderName: opts.Provider,
|
||||
ProviderURL: opts.ProviderURL,
|
||||
ClientID: opts.ClientID,
|
||||
ClientSecret: opts.ClientSecret,
|
||||
ApprovalPrompt: opts.ApprovalPrompt,
|
||||
SessionLifetimeTTL: opts.SessionLifetimeTTL,
|
||||
ProviderURL: opts.ProviderURL,
|
||||
Scopes: opts.Scopes,
|
||||
}
|
||||
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("/oauth2/callback", m.WithMethods(p.OAuthCallback, "GET"))
|
||||
// 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("/profile", m.WithMethods(p.validateExisting(p.GetProfile), "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"))
|
||||
|
||||
// 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, "/") {
|
||||
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
|
||||
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
|
||||
// a (presumably) existing user session
|
||||
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.
|
||||
|
@ -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) {
|
||||
requestLog := log.WithRequest(req, "authenticate.SignInPage")
|
||||
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
|
||||
destinationURL, _ := url.Parse(redirectURL.Query().Get("redirect_uri"))
|
||||
t := struct {
|
||||
ProviderName string
|
||||
EmailDomains []string
|
||||
AllowedDomains []string
|
||||
Redirect string
|
||||
Destination string
|
||||
Version string
|
||||
}{
|
||||
ProviderName: p.provider.Data().ProviderName,
|
||||
EmailDomains: p.EmailDomains,
|
||||
AllowedDomains: p.AllowedDomains,
|
||||
Redirect: redirectURL.String(),
|
||||
Destination: destinationURL.Host,
|
||||
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("Redirect", redirectURL.String()).
|
||||
Str("Destination", destinationURL.Host).
|
||||
Str("EmailDomains", strings.Join(p.EmailDomains, ", ")).
|
||||
Str("AllowedDomains", strings.Join(p.AllowedDomains, ", ")).
|
||||
Msg("authenticate.SignInPage")
|
||||
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")
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ func validRedirectURI(uri string, rootDomains []string) bool {
|
|||
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) {
|
||||
err := req.ParseForm()
|
||||
if err != nil {
|
||||
|
@ -56,7 +56,7 @@ func validateSignature(f http.HandlerFunc, proxyClientSecret string) http.Handle
|
|||
redirectURI := req.Form.Get("redirect_uri")
|
||||
sigVal := req.Form.Get("sig")
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ import (
|
|||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const defaultGoogleProviderURL = "https://accounts.google.com"
|
||||
|
||||
// GoogleProvider is an implementation of the Provider interface.
|
||||
type GoogleProvider struct {
|
||||
*ProviderData
|
||||
|
@ -25,7 +27,11 @@ type GoogleProvider struct {
|
|||
// NewGoogleProvider returns a new GoogleProvider and sets the provider url endpoints.
|
||||
func NewGoogleProvider(p *ProviderData) (*GoogleProvider, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package providers // import "github.com/pomerium/pomerium/internal/providers"
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
oidc "github.com/pomerium/go-oidc"
|
||||
"golang.org/x/oauth2"
|
||||
|
@ -16,7 +17,10 @@ type OIDCProvider struct {
|
|||
// NewOIDCProvider creates a new instance of an OpenID Connect provider.
|
||||
func NewOIDCProvider(p *ProviderData) (*OIDCProvider, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package providers // import "github.com/pomerium/pomerium/internal/providers"
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
oidc "github.com/pomerium/go-oidc"
|
||||
|
@ -23,7 +24,10 @@ type OktaProvider struct {
|
|||
// NewOktaProvider creates a new instance of an OpenID Connect provider.
|
||||
func NewOktaProvider(p *ProviderData) (*OktaProvider, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ func New(provider string, p *ProviderData) (Provider, error) {
|
|||
}
|
||||
return p, nil
|
||||
case OktaProviderName:
|
||||
log.Info().Msg("Okta!")
|
||||
p, err := NewOktaProvider(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -67,9 +66,8 @@ type ProviderData struct {
|
|||
ProviderName string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ProviderURL *url.URL
|
||||
ProviderURL string
|
||||
Scopes []string
|
||||
ApprovalPrompt string
|
||||
SessionLifetimeTTL time.Duration
|
||||
|
||||
verifier *oidc.IDTokenVerifier
|
||||
|
@ -227,7 +225,7 @@ func (p *ProviderData) RefreshAccessToken(refreshToken string) (string, time.Dur
|
|||
c := oauth2.Config{
|
||||
ClientID: p.ClientID,
|
||||
ClientSecret: p.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{TokenURL: p.ProviderURL.String()},
|
||||
Endpoint: oauth2.Endpoint{TokenURL: p.ProviderURL},
|
||||
}
|
||||
t := oauth2.Token{RefreshToken: refreshToken}
|
||||
ts := c.TokenSource(ctx, &t)
|
||||
|
|
|
@ -27,14 +27,15 @@ type TestProvider struct {
|
|||
|
||||
// NewTestProvider creates a new mock test provider.
|
||||
func NewTestProvider(providerURL *url.URL) *TestProvider {
|
||||
return &TestProvider{
|
||||
ProviderData: &ProviderData{
|
||||
ProviderName: "Test Provider",
|
||||
ProviderURL: &url.URL{
|
||||
host := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: providerURL.Host,
|
||||
Path: "/authorize",
|
||||
},
|
||||
}
|
||||
return &TestProvider{
|
||||
ProviderData: &ProviderData{
|
||||
ProviderName: "Test Provider",
|
||||
ProviderURL: host.String(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,12 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/pomerium/pomerium/authenticate"
|
||||
"github.com/pomerium/pomerium/internal/https"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/options"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
|
||||
"github.com/pomerium/pomerium/authenticate"
|
||||
"github.com/pomerium/pomerium/proxy"
|
||||
)
|
||||
|
||||
|
@ -24,7 +23,7 @@ var (
|
|||
func main() {
|
||||
flag.Parse()
|
||||
if *debugFlag {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
|
||||
log.SetDebugMode()
|
||||
}
|
||||
if *versionFlag {
|
||||
fmt.Printf("%s", version.FullVersion())
|
||||
|
@ -36,7 +35,7 @@ func main() {
|
|||
log.Fatal().Err(err).Msg("cmd/pomerium : failed to parse authenticator settings")
|
||||
}
|
||||
emailValidator := func(p *authenticate.Authenticator) error {
|
||||
p.Validator = options.NewEmailValidator(authOpts.EmailDomains)
|
||||
p.Validator = options.NewEmailValidator(authOpts.AllowedDomains)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -50,21 +49,13 @@ func main() {
|
|||
log.Fatal().Err(err).Msg("cmd/pomerium : failed to parse proxy settings")
|
||||
}
|
||||
|
||||
validator := func(p *proxy.Proxy) error {
|
||||
p.EmailValidator = options.NewEmailValidator(proxyOpts.EmailDomains)
|
||||
return nil
|
||||
}
|
||||
|
||||
p, err := proxy.NewProxy(proxyOpts, validator)
|
||||
p, err := proxy.NewProxy(proxyOpts)
|
||||
if err != nil {
|
||||
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.Handle(authOpts.Host+"/", authHandler)
|
||||
topMux.Handle(authOpts.RedirectURL.Host+"/", authenticator.Handler())
|
||||
topMux.Handle("/", p.Handler())
|
||||
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
|
||||
|
||||
export HOST="sso-auth.corp.beyondperimeter.com"
|
||||
export REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback"
|
||||
export PROXY_ROOT_DOMAIN=beyondperimeter.com
|
||||
export PROXY_CLIENT_ID=WLgwUNIJW6DtsnAM2ck510znU2T3l+WufPg67e50oVM=
|
||||
export PROXY_CLIENT_SECRET=gFB0qsSxxPqCtoNMuF7Q1VupJSNEq0BguxlUfT0PE+Y=
|
||||
|
||||
# Generate 256 bitrandom key to encrypt the cookie `head -c32 /dev/urandom | base64`
|
||||
export AUTH_CODE_SECRET=9wiTZq4qvmS/plYQyvzGKWPlH/UBy0DMYMA2x/zngrM=
|
||||
# The URL that the identity provider will call back after authenticating the user
|
||||
export REDIRECT_URL="https://sso-auth.corp.example.com/oauth2/callback"
|
||||
# Allow users with emails from the following domain post-fix (e.g. example.com)
|
||||
export ALLOWED_DOMAINS=*
|
||||
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
|
||||
export SHARED_SECRET=9wiTZq4qvmS/plYQyvzGKWPlH/UBy0DMYMA2x/zngrM=
|
||||
export COOKIE_SECRET=uPGHo1ujND/k3B9V6yr52Gweq3RRYfFho98jxDG5Br8=
|
||||
export COOKIE_SECURE=true
|
||||
|
||||
# Valid email domains
|
||||
export EMAIL_DOMAIN=*
|
||||
export SSO_EMAIL_DOMAIN=*
|
||||
# OKTA
|
||||
# export IDP_PROVIDER="okta
|
||||
# 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_URL="https://sso-auth.corp.beyondperimeter.com"
|
||||
export IDP_CLIENT_ID="xxx.apps.googleusercontent.com"
|
||||
export IDP_CLIENT_SECRET="xxx"
|
||||
export IDP_REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback"
|
||||
export IDP_PROVIDER_URL="https://accounts.google.com" # optional for google
|
||||
export IDP_CLIENT_ID="REPLACE-ME.googleusercontent.com"
|
||||
export IDP_CLIENT_SECRET="REPLACEME"
|
||||
|
||||
|
||||
# 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.
|
||||
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.
|
||||
func Output(w io.Writer) zerolog.Logger {
|
||||
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-host", req.Host).
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ func logRequest(proxyHost, username string, req *http.Request, url url.URL, requ
|
|||
Str("request-method", req.Method).
|
||||
Str("request-uri", uri).
|
||||
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)).
|
||||
Dur("duration", requestDuration).
|
||||
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
|
||||
// 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) {
|
||||
err := req.ParseForm()
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(rw, req, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
clientSecret := req.Form.Get("client_secret")
|
||||
clientSecret := req.Form.Get("shared_secret")
|
||||
// check the request header for the client secret
|
||||
if clientSecret == "" {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -122,7 +101,7 @@ func validRedirectURI(uri string, rootDomains []string) bool {
|
|||
|
||||
// ValidateSignature ensures the request is valid and has been signed with
|
||||
// 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) {
|
||||
err := req.ParseForm()
|
||||
if err != nil {
|
||||
|
@ -132,7 +111,7 @@ func ValidateSignature(f http.HandlerFunc, proxyClientSecret string) http.Handle
|
|||
redirectURI := req.Form.Get("redirect_uri")
|
||||
sigVal := req.Form.Get("sig")
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -154,6 +133,7 @@ func ValidateHost(h http.Handler, mux map[string]*http.Handler) http.Handler {
|
|||
|
||||
// RequireHTTPS reroutes a HTTP request to HTTPS
|
||||
// todo(bdd) : this is unreliable unless behind another reverser proxy
|
||||
// todo(bdd) : header age seems extreme
|
||||
func RequireHTTPS(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set("Strict-Transport-Security", "max-age=31536000")
|
||||
|
|
|
@ -25,7 +25,6 @@ type SessionState struct {
|
|||
|
||||
Email string `json:"email"`
|
||||
User string `json:"user"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
|
||||
// LifetimePeriodExpired returns true if the lifetime has expired
|
||||
|
|
|
@ -97,16 +97,16 @@ footer {
|
|||
|
||||
t = template.Must(t.Parse(`
|
||||
{{define "sign_in_message.html"}}
|
||||
{{if eq (len .EmailDomains) 1}}
|
||||
{{if eq (index .EmailDomains 0) "@*"}}
|
||||
{{if eq (len .AllowedDomains) 1}}
|
||||
{{if eq (index .AllowedDomains 0) "@*"}}
|
||||
<p>You may sign in with any {{.ProviderName}} account.</p>
|
||||
{{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}}
|
||||
{{else if gt (len .EmailDomains) 1}}
|
||||
{{else if gt (len .AllowedDomains) 1}}
|
||||
<p>
|
||||
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>
|
||||
{{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 {
|
||||
AuthenticateServiceURL *url.URL
|
||||
|
||||
//
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
SharedKey string
|
||||
|
||||
SignInURL *url.URL
|
||||
SignOutURL *url.URL
|
||||
RedeemURL *url.URL
|
||||
|
@ -58,12 +57,12 @@ type AuthenticateClient struct {
|
|||
}
|
||||
|
||||
// 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{
|
||||
AuthenticateServiceURL: uri,
|
||||
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
// ClientID: clientID,
|
||||
SharedKey: sharedKey,
|
||||
|
||||
SignInURL: uri.ResolveReference(&url.URL{Path: "/sign_in"}),
|
||||
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 that are validates by the authenticate service middleware
|
||||
params.Add("client_id", p.ClientID)
|
||||
params.Add("client_secret", p.ClientSecret)
|
||||
params.Add("shared_secret", p.SharedKey)
|
||||
params.Add("code", code)
|
||||
|
||||
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) {
|
||||
params := url.Values{}
|
||||
params.Add("client_id", p.ClientID)
|
||||
params.Add("client_secret", p.ClientSecret)
|
||||
params.Add("shared_secret", p.SharedKey)
|
||||
params.Add("refresh_token", refreshToken)
|
||||
var req *http.Request
|
||||
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 {
|
||||
// we validate the user's access token is valid
|
||||
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)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("user", s.Email).Msg("proxy/authenticator.ValidateSessionState : error validating session state")
|
||||
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-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
|
||||
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(fmt.Sprint(timestamp.Unix())))
|
||||
return base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
|
@ -294,7 +290,7 @@ func (p *AuthenticateClient) GetSignInURL(redirectURL *url.URL, state string) *u
|
|||
rawRedirect := redirectURL.String()
|
||||
params, _ := url.ParseQuery(a.RawQuery)
|
||||
params.Set("redirect_uri", rawRedirect)
|
||||
params.Set("client_id", p.ClientID)
|
||||
params.Set("shared_secret", p.SharedKey)
|
||||
params.Set("response_type", "code")
|
||||
params.Add("state", state)
|
||||
params.Set("ts", fmt.Sprint(now.Unix()))
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/aead"
|
||||
"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) {
|
||||
p.sessionStore.ClearSession(rw, req)
|
||||
|
||||
var scheme string
|
||||
|
||||
// Build redirect URI from request host
|
||||
if req.URL.Scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
redirectURL := &url.URL{
|
||||
Scheme: scheme,
|
||||
Scheme: "https",
|
||||
Host: req.Host,
|
||||
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
|
||||
// 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 {
|
||||
requestLog.Error().Err(err).Msg("failed to marshal csrf")
|
||||
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
|
||||
// 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 {
|
||||
requestLog.Error().Err(err).Msg("failed to encrypt cookie")
|
||||
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")
|
||||
stateParameter := &StateParameter{}
|
||||
err = p.CookieCipher.Unmarshal(encryptedState, stateParameter)
|
||||
err = p.cipher.Unmarshal(encryptedState, stateParameter)
|
||||
if err != nil {
|
||||
requestLog.Error().Err(err).Msg("could not unmarshal state")
|
||||
p.ErrorPage(rw, req, http.StatusInternalServerError, "Internal Error", "Internal Error")
|
||||
return
|
||||
}
|
||||
|
||||
c, err := req.Cookie(p.CSRFCookieName)
|
||||
c, err := p.csrfStore.GetCSRF(req)
|
||||
if err != nil {
|
||||
requestLog.Error().Err(err).Msg("failed parsing csrf cookie")
|
||||
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
|
||||
csrfParameter := &StateParameter{}
|
||||
err = p.CookieCipher.Unmarshal(encryptedCSRF, csrfParameter)
|
||||
err = p.cipher.Unmarshal(encryptedCSRF, csrfParameter)
|
||||
if err != nil {
|
||||
requestLog.Error().Err(err).Msg("couldn't unmarshal CSRF")
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
err = p.sessionStore.SaveSession(rw, req, session)
|
||||
if err != nil {
|
||||
|
@ -351,7 +333,6 @@ func (p *Proxy) Proxy(rw http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// overhead := time.Now().Sub(start)
|
||||
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) {
|
||||
requestLog.Error().Str("user", session.Email).Msg("email failed to validate, unauthorized")
|
||||
return ErrUserNotAuthorized
|
||||
}
|
||||
// if !p.EmailValidator(session.Email) {
|
||||
// requestLog.Error().Str("user", session.Email).Msg("email failed to validate, unauthorized")
|
||||
// return ErrUserNotAuthorized
|
||||
// }
|
||||
//
|
||||
// todo(bdd) : handled by authorize package
|
||||
|
||||
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-Groups", strings.Join(session.Groups, ","))
|
||||
|
||||
// stash authenticated user so that it can be logged later (see func logRequest)
|
||||
rw.Header().Set(loggingUserHeader, session.Email)
|
||||
|
|
|
@ -23,14 +23,10 @@ import (
|
|||
// Options represents the configuration options for the proxy service.
|
||||
type Options struct {
|
||||
// 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
|
||||
EmailDomains []string `envconfig:"EMAIL_DOMAIN"`
|
||||
// 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"`
|
||||
// todo(bdd) : replace with certificate based mTLS
|
||||
SharedKey string `envconfig:"SHARED_SECRET"`
|
||||
|
||||
DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"`
|
||||
|
||||
|
@ -38,7 +34,6 @@ type Options struct {
|
|||
CookieSecret string `envconfig:"COOKIE_SECRET"`
|
||||
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
|
||||
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
|
||||
CookieSecure bool `envconfig:"COOKIE_SECURE" `
|
||||
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
|
||||
|
||||
PassAccessToken bool `envconfig:"PASS_ACCESS_TOKEN"`
|
||||
|
@ -54,12 +49,11 @@ type Options struct {
|
|||
// NewOptions returns a new options struct
|
||||
var defaultOptions = &Options{
|
||||
CookieName: "_pomerium_proxy",
|
||||
CookieSecure: true,
|
||||
CookieHTTPOnly: true,
|
||||
CookieHTTPOnly: false,
|
||||
CookieExpire: time.Duration(168) * time.Hour,
|
||||
DefaultUpstreamTimeout: time.Duration(10) * time.Second,
|
||||
SessionLifetimeTTL: time.Duration(720) * time.Hour,
|
||||
SessionValidTTL: time.Duration(1) * time.Minute,
|
||||
SessionValidTTL: time.Duration(10) * time.Minute,
|
||||
GracePeriodTTL: time.Duration(3) * time.Hour,
|
||||
PassAccessToken: false,
|
||||
}
|
||||
|
@ -91,22 +85,20 @@ func (o *Options) Validate() error {
|
|||
if o.AuthenticateServiceURL == nil {
|
||||
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 == "" {
|
||||
return errors.New("missing setting: cookie-secret")
|
||||
}
|
||||
if o.ClientID == "" {
|
||||
return errors.New("missing setting: client-id")
|
||||
}
|
||||
if o.ClientSecret == "" {
|
||||
|
||||
if o.SharedKey == "" {
|
||||
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)
|
||||
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
|
||||
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.
|
||||
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
|
||||
|
||||
// services
|
||||
authenticateClient *authenticator.AuthenticateClient
|
||||
// session
|
||||
cipher aead.Cipher
|
||||
csrfStore sessions.CSRFStore
|
||||
sessionStore sessions.SessionStore
|
||||
cipher aead.Cipher
|
||||
|
||||
redirectURL *url.URL // the url to receive requests at
|
||||
templates *template.Template
|
||||
|
@ -154,13 +136,14 @@ type StateParameter struct {
|
|||
|
||||
// NewProxy takes a Proxy service from options and a validation function.
|
||||
// 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 {
|
||||
return nil, errors.New("options cannot be nil")
|
||||
}
|
||||
if err := opts.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// error explicitly handled by validate
|
||||
decodedSecret, _ := base64.StdEncoding.DecodeString(opts.CookieSecret)
|
||||
cipher, err := aead.NewMiscreantCipher(decodedSecret)
|
||||
|
@ -174,7 +157,6 @@ func NewProxy(opts *Options, optFuncs ...func(*Proxy) error) (*Proxy, error) {
|
|||
c.CookieDomain = opts.CookieDomain
|
||||
c.CookieHTTPOnly = opts.CookieHTTPOnly
|
||||
c.CookieExpire = opts.CookieExpire
|
||||
c.CookieSecure = opts.CookieSecure
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -184,9 +166,7 @@ func NewProxy(opts *Options, optFuncs ...func(*Proxy) error) (*Proxy, error) {
|
|||
|
||||
authClient := authenticator.NewAuthenticateClient(
|
||||
opts.AuthenticateServiceURL,
|
||||
// todo(bdd): fields below can be dropped as Client data provides redudent auth
|
||||
opts.ClientID,
|
||||
opts.ClientSecret,
|
||||
opts.SharedKey,
|
||||
// todo(bdd): fields below should be passed as function args
|
||||
opts.SessionLifetimeTTL,
|
||||
opts.SessionValidTTL,
|
||||
|
@ -194,21 +174,12 @@ func NewProxy(opts *Options, optFuncs ...func(*Proxy) error) (*Proxy, error) {
|
|||
)
|
||||
|
||||
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
|
||||
mux: make(map[string]*http.Handler),
|
||||
// session state
|
||||
cipher: cipher,
|
||||
csrfStore: cookieStore,
|
||||
sessionStore: cookieStore,
|
||||
cipher: cipher,
|
||||
|
||||
authenticateClient: authClient,
|
||||
redirectURL: &url.URL{Path: "/.pomerium/callback"},
|
||||
|
@ -216,13 +187,6 @@ func NewProxy(opts *Options, optFuncs ...func(*Proxy) error) (*Proxy, error) {
|
|||
PassAccessToken: opts.PassAccessToken,
|
||||
}
|
||||
|
||||
for _, optFunc := range optFuncs {
|
||||
err := optFunc(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for from, to := range opts.Routes {
|
||||
fromURL, _ := urlParse(from)
|
||||
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("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
|
||||
}
|
||||
|
||||
|
@ -261,7 +215,7 @@ var defaultUpstreamTransport = &http.Transport{
|
|||
}).DialContext,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSHandshakeTimeout: 30 * 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
|
||||
// before calling the upstream's ServeHTTP function.
|
||||
func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
requestLog := log.WithRequest(r, "proxy.ServeHTTP")
|
||||
deleteSSOCookieHeader(r, u.cookieName)
|
||||
start := time.Now()
|
||||
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.
|
||||
|
@ -295,17 +244,18 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
|
||||
proxy := httputil.NewSingleHostReverseProxy(to)
|
||||
proxy.Transport = defaultUpstreamTransport
|
||||
|
||||
director := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
req.Header.Add("X-Forwarded-Host", req.Host)
|
||||
|
||||
director(req)
|
||||
req.Host = to.Host
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
|
||||
// NewReverseProxyHandler applies handler specific options to a given
|
||||
// route.
|
||||
// NewReverseProxyHandler applies handler specific options to a given route.
|
||||
func NewReverseProxyHandler(opts *Options, reverseProxy *httputil.ReverseProxy, serviceName string) http.Handler {
|
||||
upstreamProxy := &UpstreamProxy{
|
||||
name: serviceName,
|
||||
|
|
|
@ -20,7 +20,7 @@ func TestOptionsFromEnvConfig(t *testing.T) {
|
|||
wantErr bool
|
||||
}{
|
||||
{"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},
|
||||
{"bad duration", nil, "SESSION_VALID_TTL", "1sm", true},
|
||||
}
|
||||
|
@ -127,9 +127,7 @@ func testOptions() *Options {
|
|||
return &Options{
|
||||
Routes: map[string]string{"corp.example.com": "example.com"},
|
||||
AuthenticateServiceURL: authurl,
|
||||
ClientID: "yksYDhIM7PZTvdFP3Mi3sYt2JXooTi7y0oIClBR46fs=",
|
||||
ClientSecret: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
|
||||
EmailDomains: []string{"*"},
|
||||
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
|
||||
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
|
||||
}
|
||||
}
|
||||
|
@ -147,14 +145,10 @@ func TestOptions_Validate(t *testing.T) {
|
|||
invalidCookieSecret := testOptions()
|
||||
invalidCookieSecret.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw^"
|
||||
shortCookieLength := testOptions()
|
||||
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg==" //head -c31 /dev/urandom | base64
|
||||
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
|
||||
|
||||
badClientID := testOptions()
|
||||
badClientID.ClientID = ""
|
||||
badClientSecret := testOptions()
|
||||
badClientSecret.ClientSecret = ""
|
||||
badEmailDomain := testOptions()
|
||||
badEmailDomain.EmailDomains = nil
|
||||
badSharedKey := testOptions()
|
||||
badSharedKey.SharedKey = ""
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -170,9 +164,7 @@ func TestOptions_Validate(t *testing.T) {
|
|||
{"bad - no cookie secret", emptyCookieSecret, true},
|
||||
{"bad - invalid cookie secret", invalidCookieSecret, true},
|
||||
{"bad - short cookie secret", shortCookieLength, true},
|
||||
{"bad - no client id", badClientID, true},
|
||||
{"bad - no client secret", badClientSecret, true},
|
||||
{"bad - no email domain", badEmailDomain, true},
|
||||
{"bad - no shared secret", badSharedKey, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -187,7 +179,7 @@ func TestOptions_Validate(t *testing.T) {
|
|||
func TestNewProxy(t *testing.T) {
|
||||
good := testOptions()
|
||||
shortCookieLength := testOptions()
|
||||
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg==" //head -c31 /dev/urandom | base64
|
||||
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -204,7 +196,7 @@ func TestNewProxy(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
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 {
|
||||
t.Errorf("NewProxy() error = %v, wantErr %v", err, tt.wantErr)
|
||||
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
|