Added gif to the readme.

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

40
.gitignore vendored
View file

@ -38,25 +38,16 @@ _testmain.go
# Ruby
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/*

View file

@ -1,25 +1,29 @@
<img height="200" src="./docs/logo.png" alt="logo" align="right" >
<img height="175" src="./docs/.vuepress/public/logo.svg" alt="logo" align="right" >
# Pomerium
# Pomerium : identity-aware access proxy
[![Travis CI](https://travis-ci.org/pomerium/pomerium.svg?branch=master)](https://travis-ci.org/pomerium/pomerium)
[![Go Report Card](https://goreportcard.com/badge/github.com/pomerium/pomerium)](https://goreportcard.com/report/github.com/pomerium/pomerium)
[![LICENSE](https://img.shields.io/github/license/pomerium/pomerium.svg?style=flat-square)](https://github.com/pomerium/pomerium/blob/master/LICENSE)
[![LICENSE](https://img.shields.io/github/license/pomerium/pomerium.svg)](https://github.com/pomerium/pomerium/blob/master/LICENSE)
Pomerium is a tool for managing secure access to internal applications and resources.
Pomerium is a tool for managing secure access to internal applications and resources.
Use Pomerium to:
- provide a unified ingress gateway to internal corporate applications.
- enforce dynamic access policies based on context, identity, and device state.
- 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].
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

View file

@ -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,8 +139,7 @@ type Authenticator struct {
sessionStore sessions.SessionStore
cipher aead.Cipher
redirectURL *url.URL
provider providers.Provider
provider providers.Provider
}
// NewAuthenticator creates a Authenticator struct and applies the optional functions slice to the struct.
@ -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,16 +177,15 @@ func NewAuthenticator(opts *Options, optionFuncs ...func(*Authenticator) error)
}
p := &Authenticator{
ProxyClientID: opts.ProxyClientID,
ProxyClientSecret: opts.ProxyClientSecret,
EmailDomains: opts.EmailDomains,
ProxyRootDomains: dotPrependDomains(opts.ProxyRootDomains),
CookieSecure: opts.CookieSecure,
redirectURL: opts.RedirectURL,
templates: templates.New(),
csrfStore: cookieStore,
sessionStore: cookieStore,
cipher: cipher,
SharedKey: opts.SharedKey,
AllowedDomains: opts.AllowedDomains,
ProxyRootDomains: dotPrependDomains(opts.ProxyRootDomains),
CookieSecure: opts.CookieSecure,
RedirectURL: opts.RedirectURL,
templates: templates.New(),
csrfStore: cookieStore,
sessionStore: cookieStore,
cipher: cipher,
}
// p.ServeMux = p.Handler()
p.provider, err = newProvider(opts)
@ -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)

File diff suppressed because it is too large Load diff

View file

@ -40,7 +40,7 @@ func (p *Authenticator) Handler() http.Handler {
serviceMux.HandleFunc("/start", m.WithMethods(p.OAuthStart, "GET"))
serviceMux.HandleFunc("/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,27 +85,27 @@ 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
Redirect string
Destination string
Version string
ProviderName string
AllowedDomains []string
Redirect string
Destination string
Version string
}{
ProviderName: p.provider.Data().ProviderName,
EmailDomains: p.EmailDomains,
Redirect: redirectURL.String(),
Destination: destinationURL.Host,
Version: version.FullVersion(),
ProviderName: p.provider.Data().ProviderName,
AllowedDomains: p.AllowedDomains,
Redirect: redirectURL.String(),
Destination: destinationURL.Host,
Version: version.FullVersion(),
}
requestLog.Info().
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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View file

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

View file

@ -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
View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

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

After

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

8
docs/readme.md Normal file
View file

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

View file

@ -1,26 +1,28 @@
#!/bin/bash
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'

View file

@ -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()
}

View file

@ -106,7 +106,7 @@ func logRequest(proxyHost, username string, req *http.Request, url url.URL, requ
Str("request-method", req.Method).
Str("request-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).

View file

@ -40,47 +40,26 @@ func WithMethods(f http.HandlerFunc, methods ...string) http.HandlerFunc {
}
}
// ValidateClientID checks the request body or url for the client id and returns an error
// if it does not match the proxy client id
func ValidateClientID(f http.HandlerFunc, proxyClientID string) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
// try to get the client id from the request body
err := req.ParseForm()
if err != nil {
httputil.ErrorResponse(rw, req, err.Error(), http.StatusInternalServerError)
return
}
clientID := req.FormValue("client_id")
if clientID == "" {
// try to get the clientID from the query parameters
clientID = req.URL.Query().Get("client_id")
}
if clientID != proxyClientID {
httputil.ErrorResponse(rw, req, "Invalid client_id parameter", http.StatusUnauthorized)
return
}
f(rw, req)
}
}
// ValidateClientSecret checks the request header for the client secret and returns
// 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")

View file

@ -23,9 +23,8 @@ type SessionState struct {
ValidDeadline time.Time `json:"valid_deadline"`
GracePeriodStart time.Time `json:"grace_period_start"`
Email string `json:"email"`
User string `json:"user"`
Groups []string `json:"groups"`
Email string `json:"email"`
User string `json:"user"`
}
// LifetimePeriodExpired returns true if the lifetime has expired

View file

@ -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
View file

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

View file

@ -42,15 +42,14 @@ var (
type AuthenticateClient struct {
AuthenticateServiceURL *url.URL
//
ClientID string
ClientSecret string
SignInURL *url.URL
SignOutURL *url.URL
RedeemURL *url.URL
RefreshURL *url.URL
ProfileURL *url.URL
ValidateURL *url.URL
SharedKey string
SignInURL *url.URL
SignOutURL *url.URL
RedeemURL *url.URL
RefreshURL *url.URL
ProfileURL *url.URL
ValidateURL *url.URL
SessionValidTTL time.Duration
SessionLifetimeTTL time.Duration
@ -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()))

View file

@ -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)

View file

@ -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,

View file

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

View file

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