authorize: add authorization (#59)

* authorize: authorization module adds support for per-route access policy. In this release we support the most common forms of identity based access policy: `allowed_users`, `allowed_groups`, and `allowed_domains`. In future versions, the authorization module will also support context and device based authorization policy and decisions. See website documentation for more details.
 * docs: updated `env.example` to include a `POLICY` setting example.
 * docs:  added `IDP_SERVICE_ACCOUNT` to  `env.example` .
 * docs: removed `PROXY_ROOT_DOMAIN` settings which has been replaced by `POLICY`.
 * all: removed `ALLOWED_DOMAINS` settings which has been replaced by `POLICY`. Authorization is now handled by the authorization service and is defined in the policy configuration files.
 * proxy: `ROUTES` settings which has been replaced by `POLICY`.
* internal/log: `http.Server` and `httputil.NewSingleHostReverseProxy` now uses pomerium's logging package instead of the standard library's built in one.

Closes #54
Closes #41
Closes #61
Closes #58
This commit is contained in:
Bobby DeSimone 2019-03-07 12:47:07 -08:00 committed by GitHub
parent 1187be2bf3
commit c13459bb88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1683 additions and 879 deletions

26
.codecov.yml Normal file
View file

@ -0,0 +1,26 @@
codecov:
notify:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "70...100"
status:
project: yes
patch: no
changes: no
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: "header, diff"
behavior: default
require_changes: no

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
.docker-compose.yml .*.yml
pem pem
env env
coverage.txt coverage.txt

View file

@ -2,8 +2,9 @@
## Unreleased ## Unreleased
FEATURES: **FEATURES:**
* **Authorization** : The authorization module adds support for per-route access policy. In this release we support the most common forms of identity based access policy: `allowed_users`, `allowed_groups`, and `allowed_domains`. In future versions, the authorization module will also support context and device based authorization policy and decisions. See website documentation for more details.
* **Group Support** : The authenticate service now retrieves a user's group membership information during authentication and refresh. This change may require additional identity provider configuration; all of which are described in the [updated docs](https://www.pomerium.io/docs/identity-providers.html). A brief summary of the requirements for each IdP are as follows: * **Group Support** : The authenticate service now retrieves a user's group membership information during authentication and refresh. This change may require additional identity provider configuration; all of which are described in the [updated docs](https://www.pomerium.io/docs/identity-providers.html). A brief summary of the requirements for each IdP are as follows:
- Google requires the [Admin SDK](https://developers.google.com/admin-sdk/directory/) to enabled, a service account with properly delegated access, and `IDP_SERVICE_ACCOUNT` to be set to the base64 encoded value of the service account's key file. - Google requires the [Admin SDK](https://developers.google.com/admin-sdk/directory/) to enabled, a service account with properly delegated access, and `IDP_SERVICE_ACCOUNT` to be set to the base64 encoded value of the service account's key file.
- Okta requires a `groups` claim to be added to both the `id_token` and `access_token`. No additional API calls are made. - Okta requires a `groups` claim to be added to both the `id_token` and `access_token`. No additional API calls are made.
@ -11,25 +12,22 @@ FEATURES:
- Onelogin requires the [groups](https://developers.onelogin.com/openid-connect/scopes) was supplied during authentication and that groups parameter has been mapped. Group membership is validated on refresh with the [user-info api endpoint](https://developers.onelogin.com/openid-connect/api/user-info). - Onelogin requires the [groups](https://developers.onelogin.com/openid-connect/scopes) was supplied during authentication and that groups parameter has been mapped. Group membership is validated on refresh with the [user-info api endpoint](https://developers.onelogin.com/openid-connect/api/user-info).
* **WebSocket Support** : With [Go 1.12](https://golang.org/doc/go1.12#net/http/httputil) pomerium automatically proxies WebSocket requests. * **WebSocket Support** : With [Go 1.12](https://golang.org/doc/go1.12#net/http/httputil) pomerium automatically proxies WebSocket requests.
**CHANGED**:
CHANGED: * Updated `env.example` to include a `POLICY` setting example.
* Added `IDP_SERVICE_ACCOUNT` to `env.example` .
* Removed `PROXY_ROOT_DOMAIN` settings which has been replaced by `POLICY`.
* Removed `ALLOWED_DOMAINS` settings which has been replaced by `POLICY`. Authorization is now handled by the authorization service and is defined in the policy configuration files.
* Removed `ROUTES` settings which has been replaced by `POLICY`.
* Add refresh endpoint `${url}/.pomerium/refresh` which forces a token refresh and responds with the json result. * Add refresh endpoint `${url}/.pomerium/refresh` which forces a token refresh and responds with the json result.
* Group membership added to proxy headers (`x-pomerium-authenticated-user-groups`) and (`x-pomerium-jwt-assertion`). * Group membership added to proxy headers (`x-pomerium-authenticated-user-groups`) and (`x-pomerium-jwt-assertion`).
* Default Cookie lifetime (`COOKIE_EXPIRE`) changed from 7 days to 14 hours ~ roughly one business day. * Default Cookie lifetime (`COOKIE_EXPIRE`) changed from 7 days to 14 hours ~ roughly one business day.
* Moved identity (`authenticate/providers`) into it's own internal identity package as third party identity providers are going to authorization details (group membership, user role, etc) in addition to just authentication attributes. * Moved identity (`authenticate/providers`) into its own internal identity package as third party identity providers are going to authorization details (group membership, user role, etc) in addition to just authentication attributes.
* Removed circuit breaker package. Calls that were previously wrapped with a circuit breaker fall under gRPC timeouts; which are gated by relatively short deadlines. * Removed circuit breaker package. Calls that were previously wrapped with a circuit breaker fall under gRPC timeouts; which are gated by relatively short timeouts.
* Session expiration times are truncated at the second. * Session expiration times are truncated at the second.
* **Removed gitlab provider**. We can't support groups until [this gitlab bug](https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387) is fixed. * **Removed gitlab provider**. We can't support groups until [this gitlab bug](https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387) is fixed.
* Request context is now maintained throughout request-flow via the [context package](https://golang.org/pkg/context/) enabling timeouts, request tracing, and cancellation.
IMPROVED:
* Request context is now maintained throughout request-flow via the [context package](https://golang.org/pkg/context/) enabling deadlines, request tracing, and cancellation. **FIXED:**
FIXED: * `http.Server` and `httputil.NewSingleHostReverseProxy` now uses pomerium's logging package instead of the standard library's built in one. [GH-58]
*
SECURITY:
*

View file

@ -31,11 +31,7 @@ type Options struct {
SharedKey string `envconfig:"SHARED_SECRET"` SharedKey string `envconfig:"SHARED_SECRET"`
// RedirectURL specifies the callback url following third party authentication // RedirectURL specifies the callback url following third party authentication
RedirectURL *url.URL `envconfig:"REDIRECT_URL"` RedirectURL *url.URL `envconfig:"REDIRECT_URL"`
// Coarse authorization based on user email domain
// todo(bdd) : to be replaced with authorization module
AllowedDomains []string `envconfig:"ALLOWED_DOMAINS"`
ProxyRootDomains []string `envconfig:"PROXY_ROOT_DOMAIN"` ProxyRootDomains []string `envconfig:"PROXY_ROOT_DOMAIN"`
// Session/Cookie management // Session/Cookie management
@ -84,9 +80,6 @@ func (o *Options) Validate() error {
if o.ClientSecret == "" { if o.ClientSecret == "" {
return errors.New("missing setting: client secret") return errors.New("missing setting: client secret")
} }
if len(o.AllowedDomains) == 0 {
return errors.New("missing setting email domain")
}
if len(o.ProxyRootDomains) == 0 { if len(o.ProxyRootDomains) == 0 {
return errors.New("missing setting: proxy root domain") return errors.New("missing setting: proxy root domain")
} }
@ -105,14 +98,10 @@ func (o *Options) Validate() error {
// Authenticate validates a user's identity // Authenticate validates a user's identity
type Authenticate struct { type Authenticate struct {
SharedKey string SharedKey string
RedirectURL *url.URL RedirectURL *url.URL
AllowedDomains []string
ProxyRootDomains []string ProxyRootDomains []string
Validator func(string) bool
templates *template.Template templates *template.Template
csrfStore sessions.CSRFStore csrfStore sessions.CSRFStore
sessionStore sessions.SessionStore sessionStore sessions.SessionStore
@ -122,9 +111,9 @@ type Authenticate struct {
} }
// New validates and creates a new authenticate service from a set of Options // New validates and creates a new authenticate service from a set of Options
func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate, error) { func New(opts *Options) (*Authenticate, error) {
if opts == nil { if opts == nil {
return nil, errors.New("options cannot be nil") return nil, errors.New("authenticate: options cannot be nil")
} }
if err := opts.Validate(); err != nil { if err := opts.Validate(); err != nil {
return nil, err return nil, err
@ -166,7 +155,6 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
p := &Authenticate{ p := &Authenticate{
SharedKey: opts.SharedKey, SharedKey: opts.SharedKey,
RedirectURL: opts.RedirectURL, RedirectURL: opts.RedirectURL,
AllowedDomains: opts.AllowedDomains,
ProxyRootDomains: dotPrependDomains(opts.ProxyRootDomains), ProxyRootDomains: dotPrependDomains(opts.ProxyRootDomains),
templates: templates.New(), templates: templates.New(),
@ -176,14 +164,6 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
provider: provider, provider: provider,
} }
// validation via dependency injected function
for _, optFunc := range optionFuncs {
err := optFunc(p)
if err != nil {
return nil, err
}
}
return p, nil return p, nil
} }

View file

@ -12,7 +12,6 @@ func testOptions() *Options {
redirectURL, _ := url.Parse("https://example.com/oauth2/callback") redirectURL, _ := url.Parse("https://example.com/oauth2/callback")
return &Options{ return &Options{
ProxyRootDomains: []string{"example.com"}, ProxyRootDomains: []string{"example.com"},
AllowedDomains: []string{"example.com"},
RedirectURL: redirectURL, RedirectURL: redirectURL,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=", SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
ClientID: "test-client-id", ClientID: "test-client-id",
@ -35,8 +34,6 @@ func TestOptions_Validate(t *testing.T) {
emptyClientID.ClientID = "" emptyClientID.ClientID = ""
emptyClientSecret := testOptions() emptyClientSecret := testOptions()
emptyClientSecret.ClientSecret = "" emptyClientSecret.ClientSecret = ""
allowedDomains := testOptions()
allowedDomains.AllowedDomains = nil
proxyRootDomains := testOptions() proxyRootDomains := testOptions()
proxyRootDomains.ProxyRootDomains = nil proxyRootDomains.ProxyRootDomains = nil
emptyCookieSecret := testOptions() emptyCookieSecret := testOptions()
@ -63,7 +60,6 @@ func TestOptions_Validate(t *testing.T) {
{"no shared secret", badSharedKey, true}, {"no shared secret", badSharedKey, true},
{"no client id", emptyClientID, true}, {"no client id", emptyClientID, true},
{"no client secret", emptyClientSecret, true}, {"no client secret", emptyClientSecret, true},
{"empty allowed domains", allowedDomains, true},
{"empty root domains", proxyRootDomains, true}, {"empty root domains", proxyRootDomains, true},
} }
for _, tt := range tests { for _, tt := range tests {

View file

@ -1,3 +1,5 @@
//go:generate protoc -I ../proto/authenticate --go_out=plugins=grpc:../proto/authenticate ../proto/authenticate/authenticate.proto
package authenticate // import "github.com/pomerium/pomerium/authenticate" package authenticate // import "github.com/pomerium/pomerium/authenticate"
import ( import (
"context" "context"
@ -20,7 +22,7 @@ func (p *Authenticate) Authenticate(ctx context.Context, in *pb.AuthenticateRequ
return newSessionProto, nil return newSessionProto, nil
} }
// Validate locally validates a JWT id token; does NOT do nonce or revokation validation. // Validate locally validates a JWT id_token; does NOT do nonce or revokation validation.
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func (p *Authenticate) Validate(ctx context.Context, in *pb.ValidateRequest) (*pb.ValidateReply, error) { func (p *Authenticate) Validate(ctx context.Context, in *pb.ValidateRequest) (*pb.ValidateReply, error) {
isValid, err := p.provider.Validate(ctx, in.IdToken) isValid, err := p.provider.Validate(ctx, in.IdToken)

View file

@ -78,9 +78,6 @@ func TestAuthenticate_Refresh(t *testing.T) {
false}, false},
{"test error", &identity.MockProvider{RefreshError: errors.New("hi")}, &pb.Session{RefreshToken: "refresh token", RefreshDeadline: fixedProtoTime, LifetimeDeadline: fixedProtoTime}, nil, true}, {"test error", &identity.MockProvider{RefreshError: errors.New("hi")}, &pb.Session{RefreshToken: "refresh token", RefreshDeadline: fixedProtoTime, LifetimeDeadline: fixedProtoTime}, nil, true},
{"test catch nil", nil, nil, nil, true}, {"test catch nil", nil, nil, nil, true},
// {"test error", "error", nil, true},
// {"test bad time", "bad time", nil, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View file

@ -81,7 +81,6 @@ func (a *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*se
return nil, err return nil, err
} }
// check if session refresh period is up
if session.RefreshPeriodExpired() { if session.RefreshPeriodExpired() {
newSession, err := a.provider.Refresh(r.Context(), session) newSession, err := a.provider.Refresh(r.Context(), session)
if err != nil { if err != nil {
@ -111,12 +110,6 @@ func (a *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*se
} }
} }
// authenticate really should not be in the business of authorization
// todo(bdd) : remove when authorization module added
if !a.Validator(session.Email) {
log.FromRequest(r).Error().Msg("invalid email user")
return nil, httputil.ErrUserNotAuthorized
}
return session, nil return session, nil
} }
@ -238,12 +231,7 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
// OAuthStart starts the authenticate process by redirecting to the identity provider. // OAuthStart starts the authenticate process by redirecting to the identity provider.
// https://tools.ietf.org/html/rfc6749#section-4.2.1 // https://tools.ietf.org/html/rfc6749#section-4.2.1
func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) { func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
authRedirectURL, err := url.Parse(r.URL.Query().Get("redirect_uri")) authRedirectURL := a.RedirectURL.ResolveReference(r.URL)
if err != nil {
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
return
}
authRedirectURL = a.RedirectURL.ResolveReference(r.URL)
nonce := fmt.Sprintf("%x", cryptutil.GenerateKey()) nonce := fmt.Sprintf("%x", cryptutil.GenerateKey())
a.csrfStore.SetCSRF(w, r, nonce) a.csrfStore.SetCSRF(w, r, nonce)
@ -345,11 +333,6 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Invalid Redirect URI"} return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Invalid Redirect URI"}
} }
// Set cookie, or deny: validates the session email and group
if !a.Validator(session.Email) {
log.FromRequest(r).Error().Err(err).Str("email", session.Email).Msg("invalid email permissions denied")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "You don't have access"}
}
err = a.sessionStore.SaveSession(w, r, session) err = a.sessionStore.SaveSession(w, r, session)
if err != nil { if err != nil {
log.Error().Err(err).Msg("internal error") log.Error().Err(err).Msg("internal error")

View file

@ -17,15 +17,10 @@ import (
"github.com/pomerium/pomerium/internal/templates" "github.com/pomerium/pomerium/internal/templates"
) )
// mocks for validator func
func trueValidator(s string) bool { return true }
func falseValidator(s string) bool { return false }
func testAuthenticate() *Authenticate { func testAuthenticate() *Authenticate {
var auth Authenticate var auth Authenticate
auth.RedirectURL, _ = url.Parse("https://auth.example.com/oauth/callback") auth.RedirectURL, _ = url.Parse("https://auth.example.com/oauth/callback")
auth.SharedKey = "IzY7MOZwzfOkmELXgozHDKTxoT3nOYhwkcmUVINsRww=" auth.SharedKey = "IzY7MOZwzfOkmELXgozHDKTxoT3nOYhwkcmUVINsRww="
auth.AllowedDomains = []string{"*"}
auth.ProxyRootDomains = []string{"example.com"} auth.ProxyRootDomains = []string{"example.com"}
auth.templates = templates.New() auth.templates = templates.New()
return &auth return &auth
@ -86,66 +81,25 @@ func TestAuthenticate_authenticate(t *testing.T) {
}} }}
tests := []struct { tests := []struct {
name string name string
session sessions.SessionStore session sessions.SessionStore
provider identity.MockProvider provider identity.MockProvider
validator func(string) bool want *sessions.SessionState
want *sessions.SessionState wantErr bool
wantErr bool
}{ }{
{"good", goodSession, identity.MockProvider{ValidateResponse: true}, trueValidator, nil, false}, {"good", goodSession, identity.MockProvider{ValidateResponse: true}, nil, false},
{"good but fails validation", goodSession, identity.MockProvider{ValidateResponse: true}, falseValidator, nil, true}, {"can't load session", &sessions.MockSessionStore{LoadError: errors.New("error")}, identity.MockProvider{ValidateResponse: true}, nil, true},
{"can't load session", &sessions.MockSessionStore{LoadError: errors.New("error")}, identity.MockProvider{ValidateResponse: true}, trueValidator, nil, true}, {"validation fails", goodSession, identity.MockProvider{ValidateResponse: false}, nil, true},
{"validation fails", goodSession, identity.MockProvider{ValidateResponse: false}, trueValidator, nil, true}, {"session fails after good validation", &sessions.MockSessionStore{SaveError: errors.New("error"), Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second)}}, identity.MockProvider{ValidateResponse: true}, nil, true},
{"session fails after good validation", &sessions.MockSessionStore{ {"refresh expired", expiredRefresPeriod, identity.MockProvider{ValidateResponse: true, RefreshResponse: &sessions.SessionState{AccessToken: "new token", LifetimeDeadline: time.Now()}}, nil, false},
SaveError: errors.New("error"), {"refresh expired refresh error", expiredRefresPeriod, identity.MockProvider{ValidateResponse: true, RefreshError: errors.New("error")}, nil, true},
Session: &sessions.SessionState{ {"refresh expired failed save", &sessions.MockSessionStore{SaveError: errors.New("error"), Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * -time.Second)}}, identity.MockProvider{ValidateResponse: true, RefreshResponse: &sessions.SessionState{AccessToken: "new token", LifetimeDeadline: time.Now()}}, nil, true},
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * time.Second),
}}, identity.MockProvider{ValidateResponse: true},
trueValidator, nil, true},
{"refresh expired",
expiredRefresPeriod,
identity.MockProvider{
ValidateResponse: true,
RefreshResponse: &sessions.SessionState{
AccessToken: "new token",
LifetimeDeadline: time.Now(),
},
},
trueValidator, nil, false},
{"refresh expired refresh error",
expiredRefresPeriod,
identity.MockProvider{
ValidateResponse: true,
RefreshError: errors.New("error"),
},
trueValidator, nil, true},
{"refresh expired failed save",
&sessions.MockSessionStore{
SaveError: errors.New("error"),
Session: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * -time.Second),
}},
identity.MockProvider{
ValidateResponse: true,
RefreshResponse: &sessions.SessionState{
AccessToken: "new token",
LifetimeDeadline: time.Now(),
},
},
trueValidator, nil, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
p := &Authenticate{ p := &Authenticate{
sessionStore: tt.session, sessionStore: tt.session,
provider: tt.provider, provider: tt.provider,
Validator: tt.validator,
} }
r := httptest.NewRequest("GET", "/auth", nil) r := httptest.NewRequest("GET", "/auth", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -161,11 +115,10 @@ func TestAuthenticate_authenticate(t *testing.T) {
func TestAuthenticate_SignIn(t *testing.T) { func TestAuthenticate_SignIn(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
session sessions.SessionStore session sessions.SessionStore
provider identity.MockProvider provider identity.MockProvider
validator func(string) bool wantCode int
wantCode int
}{ }{
{"good", {"good",
&sessions.MockSessionStore{ &sessions.MockSessionStore{
@ -175,7 +128,7 @@ func TestAuthenticate_SignIn(t *testing.T) {
RefreshDeadline: time.Now().Add(10 * time.Second), RefreshDeadline: time.Now().Add(10 * time.Second),
}}, }},
identity.MockProvider{ValidateResponse: true}, identity.MockProvider{ValidateResponse: true},
trueValidator,
http.StatusForbidden}, http.StatusForbidden},
{"session fails after good validation", &sessions.MockSessionStore{ {"session fails after good validation", &sessions.MockSessionStore{
SaveError: errors.New("error"), SaveError: errors.New("error"),
@ -184,14 +137,13 @@ func TestAuthenticate_SignIn(t *testing.T) {
RefreshToken: "RefreshToken", RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * time.Second), RefreshDeadline: time.Now().Add(10 * time.Second),
}}, identity.MockProvider{ValidateResponse: true}, }}, identity.MockProvider{ValidateResponse: true},
trueValidator, http.StatusBadRequest}, http.StatusBadRequest},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
a := &Authenticate{ a := &Authenticate{
sessionStore: tt.session, sessionStore: tt.session,
provider: tt.provider, provider: tt.provider,
Validator: tt.validator,
RedirectURL: uriParse("http://www.pomerium.io"), RedirectURL: uriParse("http://www.pomerium.io"),
csrfStore: &sessions.MockCSRFStore{}, csrfStore: &sessions.MockCSRFStore{},
SharedKey: "secret", SharedKey: "secret",
@ -592,11 +544,9 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
code string code string
state string state string
validDomains []string validDomains []string
validator func(string) bool session sessions.SessionStore
provider identity.MockProvider
session sessions.SessionStore csrfStore sessions.MockCSRFStore
provider identity.MockProvider
csrfStore sessions.MockCSRFStore
want string want string
wantErr bool wantErr bool
@ -607,7 +557,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code", "code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"}, []string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{}, &sessions.MockSessionStore{},
identity.MockProvider{ identity.MockProvider{
AuthenticateResponse: sessions.SessionState{ AuthenticateResponse: sessions.SessionState{
@ -629,7 +579,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code", "code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"}, []string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{}, &sessions.MockSessionStore{},
identity.MockProvider{ identity.MockProvider{
AuthenticateResponse: sessions.SessionState{ AuthenticateResponse: sessions.SessionState{
@ -652,7 +602,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code", "code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"}, []string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{}, &sessions.MockSessionStore{},
identity.MockProvider{ identity.MockProvider{
AuthenticateResponse: sessions.SessionState{ AuthenticateResponse: sessions.SessionState{
@ -674,7 +624,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code", "code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"}, []string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{}, &sessions.MockSessionStore{},
identity.MockProvider{ identity.MockProvider{
AuthenticateError: errors.New("error"), AuthenticateError: errors.New("error"),
@ -691,30 +641,8 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code", "code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"}, []string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{SaveError: errors.New("error")},
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
Email: "blah@blah.com",
RefreshDeadline: time.Now().Add(10 * time.Second), &sessions.MockSessionStore{SaveError: errors.New("error")},
}},
sessions.MockCSRFStore{
ResponseCSRF: "csrf",
Cookie: &http.Cookie{Value: "nonce"}},
"",
true,
},
{"failed email validation",
http.MethodGet,
"",
"code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"},
falseValidator,
&sessions.MockSessionStore{},
identity.MockProvider{ identity.MockProvider{
AuthenticateResponse: sessions.SessionState{ AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken", AccessToken: "AccessToken",
@ -736,7 +664,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code", "code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"}, []string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{}, &sessions.MockSessionStore{},
identity.MockProvider{ identity.MockProvider{
AuthenticateResponse: sessions.SessionState{ AuthenticateResponse: sessions.SessionState{
@ -758,7 +686,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"", "",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")), base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"}, []string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{}, &sessions.MockSessionStore{},
identity.MockProvider{ identity.MockProvider{
AuthenticateResponse: sessions.SessionState{ AuthenticateResponse: sessions.SessionState{
@ -780,7 +708,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code", "code",
"nonce:https://corp.pomerium.io", "nonce:https://corp.pomerium.io",
[]string{"pomerium.io"}, []string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{}, &sessions.MockSessionStore{},
identity.MockProvider{ identity.MockProvider{
AuthenticateResponse: sessions.SessionState{ AuthenticateResponse: sessions.SessionState{
@ -802,7 +730,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code", "code",
base64.URLEncoding.EncodeToString([]byte("nonce")), base64.URLEncoding.EncodeToString([]byte("nonce")),
[]string{"pomerium.io"}, []string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{}, &sessions.MockSessionStore{},
identity.MockProvider{ identity.MockProvider{
AuthenticateResponse: sessions.SessionState{ AuthenticateResponse: sessions.SessionState{
@ -824,7 +752,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code", "code",
base64.URLEncoding.EncodeToString([]byte("nonce:corp.pomerium.io")), base64.URLEncoding.EncodeToString([]byte("nonce:corp.pomerium.io")),
[]string{"pomerium.io"}, []string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{}, &sessions.MockSessionStore{},
identity.MockProvider{ identity.MockProvider{
AuthenticateResponse: sessions.SessionState{ AuthenticateResponse: sessions.SessionState{
@ -848,7 +776,6 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
csrfStore: tt.csrfStore, csrfStore: tt.csrfStore,
provider: tt.provider, provider: tt.provider,
ProxyRootDomains: tt.validDomains, ProxyRootDomains: tt.validDomains,
Validator: tt.validator,
} }
u, _ := url.Parse("/oauthGet") u, _ := url.Parse("/oauthGet")
params, _ := url.ParseQuery(u.RawQuery) params, _ := url.ParseQuery(u.RawQuery)

107
authorize/authorize.go Normal file
View file

@ -0,0 +1,107 @@
package authorize // import "github.com/pomerium/pomerium/authorize"
import (
"encoding/base64"
"errors"
"fmt"
"github.com/pomerium/envconfig"
"github.com/pomerium/pomerium/internal/policy"
)
// Options contains configuration settings for the authorize service.
type Options struct {
// SharedKey is used to validate requests between services
SharedKey string `envconfig:"SHARED_SECRET" required:"true"`
// Policy is a base64 encoded yaml blob which enumerates
// per-route access control policies.
Policy string `envconfig:"POLICY"`
PolicyFile string `envconfig:"POLICY_FILE"`
}
// OptionsFromEnvConfig creates an authorize service options from environmental
// variables.
func OptionsFromEnvConfig() (*Options, error) {
o := new(Options)
if err := envconfig.Process("", o); err != nil {
return nil, err
}
return o, nil
}
// Validate checks to see if configuration values are valid for the
// authorize service. Returns first error, if found.
func (o *Options) Validate() error {
decoded, err := base64.StdEncoding.DecodeString(o.SharedKey)
if err != nil {
return fmt.Errorf("authorize: `SHARED_SECRET` setting is invalid base64: %v", err)
}
if len(decoded) != 32 {
return fmt.Errorf("authorize: `SHARED_SECRET` want 32 but got %d bytes", len(decoded))
}
if o.Policy == "" && o.PolicyFile == "" {
return errors.New("authorize: either `POLICY` or `POLICY_FILE` must be non-nil")
}
if o.Policy != "" {
confBytes, err := base64.StdEncoding.DecodeString(o.Policy)
if err != nil {
return fmt.Errorf("authorize: `POLICY` is invalid base64 %v", err)
}
_, err = policy.FromConfig(confBytes)
if err != nil {
return fmt.Errorf("authorize: `POLICY` %v", err)
}
}
if o.PolicyFile != "" {
_, err = policy.FromConfigFile(o.PolicyFile)
if err != nil {
return fmt.Errorf("authorize: `POLICY_FILE` %v", err)
}
}
return nil
}
// Authorize struct holds
type Authorize struct {
SharedKey string
identityAccess IdentityValidator
// contextValidator
// deviceValidator
}
// New validates and creates a new Authorize service from a set of Options
func New(opts *Options) (*Authorize, error) {
if opts == nil {
return nil, errors.New("authorize: options cannot be nil")
}
if err := opts.Validate(); err != nil {
return nil, err
}
// errors handled by validate
sharedKey, _ := base64.StdEncoding.DecodeString(opts.SharedKey)
var policies []policy.Policy
if opts.Policy != "" {
confBytes, _ := base64.StdEncoding.DecodeString(opts.Policy)
policies, _ = policy.FromConfig(confBytes)
} else {
policies, _ = policy.FromConfigFile(opts.PolicyFile)
}
return &Authorize{
SharedKey: string(sharedKey),
identityAccess: NewIdentityWhitelist(policies),
}, nil
}
// ValidIdentity returns if an identity is authorized to access a route resource.
func (a *Authorize) ValidIdentity(route string, identity *Identity) bool {
return a.identityAccess.Valid(route, identity)
}
// NewIdentityWhitelist returns an indentity validator.
// todo(bdd) : a radix-tree implementation is probably more efficient
func NewIdentityWhitelist(policies []policy.Policy) IdentityValidator {
return newIdentityWhitelistMap(policies)
}

View file

@ -0,0 +1,93 @@
package authorize
import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"
)
func TestOptionsFromEnvConfig(t *testing.T) {
t.Parallel()
os.Clearenv()
tests := []struct {
name string
want *Options
envKey string
envValue string
wantErr bool
}{
{"shared secret missing", nil, "", "", true},
{"with secret", &Options{SharedKey: "aGkK"}, "SHARED_SECRET", "aGkK", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envKey != "" {
os.Setenv(tt.envKey, tt.envValue)
defer os.Unsetenv(tt.envKey)
}
got, err := OptionsFromEnvConfig()
if (err != nil) != tt.wantErr {
t.Errorf("OptionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("OptionsFromEnvConfig() = %v, want %v", got, tt.want)
}
})
}
}
func TestNew(t *testing.T) {
t.Parallel()
content := []byte(`[{"from": "pomerium.io","to":"httpbin.org"}]`)
tmpfile, err := ioutil.TempFile("", "example")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write(content); err != nil {
log.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
log.Fatal(err)
}
tests := []struct {
name string
SharedKey string
Policy string
PolicyFile string
wantErr bool
}{
{"good", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "WwogIHsKICAgICJyb3V0ZXMiOiAiaHR0cDovL3BvbWVyaXVtLmlvIgogIH0KXQ==", "", false},
{"bad shared secret", "AZA85podM73CjLCjViDNz1EUvvejKpWp7Hysr0knXA==", "WwogIHsKICAgICJyb3V0ZXMiOiAiaHR0cDovL3BvbWVyaXVtLmlvIgogIH0KXQ==", "", true},
{"really bad shared secret", "sup", "WwogIHsKICAgICJyb3V0ZXMiOiAiaHR0cDovL3BvbWVyaXVtLmlvIgogIH0KXQ==", "", true},
{"bad base64 policy", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "WwogIHsKICAgICJyb3V0ZXMiOiAiaHR0cDovL3BvbWVyaXVtLmlvIgogIH0KXQ^=", "", true},
{"bad json", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "e30=", "", true},
{"no policies", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "", "", true},
{"good policy file", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "", "./testdata/basic.json", true},
{"bad policy file, directory", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "", "./testdata/", true},
{"good policy", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "WwogIHsKICAgICJyb3V0ZXMiOiAiaHR0cDovL3BvbWVyaXVtLmlvIgogIH0KXQ==", "", false},
{"good file", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "", tmpfile.Name(), false},
{"validation error, short secret", "AZA85podM73CjLCjViDNz1EUvvejKpWp7Hysr0knXA==", "", "", true},
{"nil options", "", "", "", true}, // special case
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &Options{SharedKey: tt.SharedKey, Policy: tt.Policy, PolicyFile: tt.PolicyFile}
if tt.name == "nil options" {
o = nil
}
_, err := New(o)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return
}
// if !reflect.DeepEqual(got, tt.want) {
// t.Errorf("New() = %v, want %v", got, tt.want)
// }
})
}
}

18
authorize/gprc.go Normal file
View file

@ -0,0 +1,18 @@
//go:generate protoc -I ../proto/authorize --go_out=plugins=grpc:../proto/authorize ../proto/authorize/authorize.proto
package authorize // import "github.com/pomerium/pomerium/authorize"
import (
"context"
pb "github.com/pomerium/pomerium/proto/authorize"
"github.com/pomerium/pomerium/internal/log"
)
// Authorize validates the user identity, device, and context of a request for
// a given route. Currently only checks identity.
func (a *Authorize) Authorize(ctx context.Context, in *pb.AuthorizeRequest) (*pb.AuthorizeReply, error) {
ok := a.ValidIdentity(in.Route, &Identity{in.User, in.Email, in.Groups})
log.Debug().Str("route", in.Route).Strs("groups", in.Groups).Str("email", in.Email).Bool("Valid?", ok).Msg("authorize/grpc")
return &pb.AuthorizeReply{IsValid: ok}, nil
}

39
authorize/gprc_test.go Normal file
View file

@ -0,0 +1,39 @@
//go:generate protoc -I ../proto/authorize --go_out=plugins=grpc:../proto/authorize ../proto/authorize/authorize.proto
package authorize
import (
"context"
"reflect"
"testing"
pb "github.com/pomerium/pomerium/proto/authorize"
)
func TestAuthorize_Authorize(t *testing.T) {
t.Parallel()
tests := []struct {
name string
SharedKey string
identityAccess IdentityValidator
in *pb.AuthorizeRequest
want *pb.AuthorizeReply
wantErr bool
}{
{"valid authorization request", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", &MockIdentityValidator{ValidResponse: true}, &pb.AuthorizeRequest{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.AuthorizeReply{IsValid: true}, false},
{"invalid authorization request", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", &MockIdentityValidator{ValidResponse: false}, &pb.AuthorizeRequest{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.AuthorizeReply{IsValid: false}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &Authorize{SharedKey: tt.SharedKey, identityAccess: tt.identityAccess}
got, err := a.Authorize(context.Background(), tt.in)
if (err != nil) != tt.wantErr {
t.Errorf("Authorize.Authorize() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Authorize.Authorize() = %v, want %v", got, tt.want)
}
})
}
}

127
authorize/identity.go Normal file
View file

@ -0,0 +1,127 @@
package authorize // import "github.com/pomerium/pomerium/authorize"
import (
"fmt"
"strings"
"sync"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/policy"
)
// Identity contains a user's identity information.
type Identity struct {
User string
Email string
Groups []string
}
// EmailDomain returns the domain of the identity's email.
func (i *Identity) EmailDomain() string {
if i.Email == "" {
return ""
}
comp := strings.Split(i.Email, "@")
if len(comp) != 2 || comp[0] == "" {
return ""
}
return comp[1]
}
// IdentityValidator provides an interface to check whether a user has access
// to a given route.
type IdentityValidator interface {
Valid(string, *Identity) bool
}
type identityWhitelist struct {
sync.RWMutex
m map[string]bool
}
// newIdentityWhitelistMap takes a slice of policies and creates a hashmap of identity
// authorizations per-route for each allowed group, domain, and email.
func newIdentityWhitelistMap(policies []policy.Policy) *identityWhitelist {
var im identityWhitelist
im.m = make(map[string]bool, len(policies)*3)
for _, p := range policies {
for _, group := range p.AllowedGroups {
log.Debug().Str("route", p.From).Str("group", group).Msg("add group")
im.PutGroup(p.From, group)
}
for _, domain := range p.AllowedDomains {
im.PutDomain(p.From, domain)
log.Debug().Str("route", p.From).Str("group", domain).Msg("add domain")
}
for _, email := range p.AllowedEmails {
im.PutEmail(p.From, email)
log.Debug().Str("route", p.From).Str("group", email).Msg("add email")
}
}
return &im
}
// Valid reports whether an identity has valid access for a given route.
func (m *identityWhitelist) Valid(route string, i *Identity) bool {
if ok := m.Domain(route, i.EmailDomain()); ok {
return ok
}
if ok := m.Email(route, i.Email); ok {
return ok
}
for _, group := range i.Groups {
if ok := m.Group(route, group); ok {
return ok
}
}
return false
}
// Group retrieves per-route access given a group name.
func (m *identityWhitelist) Group(route, group string) bool {
m.RLock()
defer m.RUnlock()
return m.m[fmt.Sprintf("%s|group:%s", route, group)]
}
// PutGroup adds an access entry for a route given a group name.
func (m *identityWhitelist) PutGroup(route, group string) {
m.Lock()
m.m[fmt.Sprintf("%s|group:%s", route, group)] = true
m.Unlock()
}
// Domain retrieves per-route access given a domain name.
func (m *identityWhitelist) Domain(route, domain string) bool {
m.RLock()
defer m.RUnlock()
return m.m[fmt.Sprintf("%s|domain:%s", route, domain)]
}
// PutDomain adds an access entry for a route given a domain name.
func (m *identityWhitelist) PutDomain(route, domain string) {
m.Lock()
m.m[fmt.Sprintf("%s|domain:%s", route, domain)] = true
m.Unlock()
}
// Email retrieves per-route access given a user's email.
func (m *identityWhitelist) Email(route, email string) bool {
m.RLock()
defer m.RUnlock()
return m.m[fmt.Sprintf("%s|email:%s", route, email)]
}
// PutEmail adds an access entry for a route given a user's email.
func (m *identityWhitelist) PutEmail(route, email string) {
m.Lock()
m.m[fmt.Sprintf("%s|email:%s", route, email)] = true
m.Unlock()
}
// MockIdentityValidator is a mock implementation of IdentityValidator
type MockIdentityValidator struct{ ValidResponse bool }
// Valid is a mock implementation IdentityValidator's Valid method
func (mv *MockIdentityValidator) Valid(u string, i *Identity) bool { return mv.ValidResponse }

View file

@ -0,0 +1,62 @@
package authorize
import (
"testing"
"github.com/pomerium/pomerium/internal/policy"
)
func TestIdentity_EmailDomain(t *testing.T) {
t.Parallel()
tests := []struct {
name string
Email string
want string
}{
{"simple", "user@pomerium.io", "pomerium.io"},
{"period malformed", "user@.io", ".io"},
{"empty", "", ""},
{"empty first part", "@uhoh.com", ""},
{"empty second part", "uhoh@", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
i := &Identity{Email: tt.Email}
if got := i.EmailDomain(); got != tt.want {
t.Errorf("Identity.EmailDomain() = %v, want %v", got, tt.want)
}
})
}
}
func Test_IdentityWhitelistMap(t *testing.T) {
t.Parallel()
tests := []struct {
name string
policies []policy.Policy
route string
Identity *Identity
want bool
}{
{"valid domain", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, true},
{"invalid domain prepend", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "a@1example.com"}, false},
{"invalid domain postpend", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com2"}, false},
{"valid group", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"admin"}}, true},
{"invalid group", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyone"}}, false},
{"invalid empty", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{""}}, false},
{"valid group multiple", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyone", "admin"}}, true},
{"invalid group multiple", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyones", "sadmin"}}, false},
{"valid user email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, true},
{"invalid user email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "user2@example.com"}, false},
{"empty everything", []policy.Policy{{From: "example.com"}}, "example.com", &Identity{Email: "user2@example.com"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wl := NewIdentityWhitelist(tt.policies)
if got := wl.Valid(tt.route, tt.Identity); got != tt.want {
t.Errorf("IdentityACLMap.Allowed() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -9,12 +9,13 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"github.com/pomerium/pomerium/authenticate" "github.com/pomerium/pomerium/authenticate"
"github.com/pomerium/pomerium/authorize"
"github.com/pomerium/pomerium/internal/https" "github.com/pomerium/pomerium/internal/https"
"github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware" "github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/options"
"github.com/pomerium/pomerium/internal/version" "github.com/pomerium/pomerium/internal/version"
pb "github.com/pomerium/pomerium/proto/authenticate" pbAuthenticate "github.com/pomerium/pomerium/proto/authenticate"
pbAuthorize "github.com/pomerium/pomerium/proto/authorize"
"github.com/pomerium/pomerium/proxy" "github.com/pomerium/pomerium/proxy"
) )
@ -36,7 +37,7 @@ func main() {
fmt.Printf("%s", version.FullVersion()) fmt.Printf("%s", version.FullVersion())
os.Exit(0) os.Exit(0)
} }
log.Info().Str("version", version.FullVersion()).Str("service-mode", mainOpts.Services).Msg("cmd/pomerium") log.Info().Str("version", version.FullVersion()).Str("service", mainOpts.Services).Msg("cmd/pomerium")
grpcAuth := middleware.NewSharedSecretCred(mainOpts.SharedKey) grpcAuth := middleware.NewSharedSecretCred(mainOpts.SharedKey)
grpcOpts := []grpc.ServerOption{grpc.UnaryInterceptor(grpcAuth.ValidateRequest)} grpcOpts := []grpc.ServerOption{grpc.UnaryInterceptor(grpcAuth.ValidateRequest)}
@ -45,22 +46,29 @@ func main() {
var authenticateService *authenticate.Authenticate var authenticateService *authenticate.Authenticate
var authHost string var authHost string
if mainOpts.Services == "all" || mainOpts.Services == "authenticate" { if mainOpts.Services == "all" || mainOpts.Services == "authenticate" {
authOpts, err := authenticate.OptionsFromEnvConfig() opts, err := authenticate.OptionsFromEnvConfig()
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: authenticate settings") log.Fatal().Err(err).Msg("cmd/pomerium: authenticate settings")
} }
emailValidator := func(p *authenticate.Authenticate) error { authenticateService, err = authenticate.New(opts)
p.Validator = options.NewEmailValidator(authOpts.AllowedDomains)
return nil
}
authenticateService, err = authenticate.New(authOpts, emailValidator)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: new authenticate") log.Fatal().Err(err).Msg("cmd/pomerium: new authenticate")
} }
authHost = authOpts.RedirectURL.Host authHost = opts.RedirectURL.Host
pb.RegisterAuthenticatorServer(grpcServer, authenticateService) pbAuthenticate.RegisterAuthenticatorServer(grpcServer, authenticateService)
}
var authorizeService *authorize.Authorize
if mainOpts.Services == "all" || mainOpts.Services == "authorize" {
opts, err := authorize.OptionsFromEnvConfig()
if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: authorize settings")
}
authorizeService, err = authorize.New(opts)
if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: new authorize")
}
pbAuthorize.RegisterAuthorizerServer(grpcServer, authorizeService)
} }
var proxyService *proxy.Proxy var proxyService *proxy.Proxy
@ -74,7 +82,10 @@ func main() {
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: new proxy") log.Fatal().Err(err).Msg("cmd/pomerium: new proxy")
} }
// cleanup our RPC services
defer proxyService.AuthenticateClient.Close() defer proxyService.AuthenticateClient.Close()
defer proxyService.AuthorizeClient.Close()
} }
topMux := http.NewServeMux() topMux := http.NewServeMux()

View file

@ -64,6 +64,7 @@ func isValidService(service string) bool {
case case
"all", "all",
"proxy", "proxy",
"authorize",
"authenticate": "authenticate":
return true return true
} }

View file

@ -55,7 +55,7 @@ func Test_isValidService(t *testing.T) {
{"all", "all", true}, {"all", "all", true},
{"authenticate", "authenticate", true}, {"authenticate", "authenticate", true},
{"authenticate bad case", "AuThenticate", false}, {"authenticate bad case", "AuThenticate", false},
{"authorize not yet implemented", "authorize", false}, {"authorize implemented", "authorize", true},
{"jiberish", "xd23", false}, {"jiberish", "xd23", false},
} }
for _, tt := range tests { for _, tt := range tests {

View file

@ -19,7 +19,7 @@ Global settings are configuration variables that are shared by all services.
- Environmental Variable: `SERVICES` - Environmental Variable: `SERVICES`
- Type: `string` - Type: `string`
- Default: `all` - Default: `all`
- Options: `all` `authenticate` or `proxy` - Options: `all` `authenticate` `authorize` or `proxy`
Service mode sets the pomerium service(s) to run. If testing, you may want to set to `all` and run pomerium in "all-in-one mode." In production, you'll likely want to spin of several instances of each service mode for high availability. Service mode sets the pomerium service(s) to run. If testing, you may want to set to `all` and run pomerium in "all-in-one mode." In production, you'll likely want to spin of several instances of each service mode for high availability.
@ -43,6 +43,17 @@ Shared Secret is the base64 encoded 256-bit key used to mutually authenticate re
head -c32 /dev/urandom | base64 head -c32 /dev/urandom | base64
``` ```
### Policy
- Environmental Variable: either `POLICY` or `POLICY_FILE`
- Type: [base64 encoded] `string` or relative file location
- Filetype: `json` or `yaml`
- Required
Policy contains the routes, and their access policies. For example,
<<< @/policy.example.yaml
### Debug ### Debug
- Environmental Variable: `POMERIUM_DEBUG` - Environmental Variable: `POMERIUM_DEBUG`
@ -51,7 +62,7 @@ head -c32 /dev/urandom | base64
By default, JSON encoded logs are produced. Debug enables colored, human-readable, and more verbose logs to be streamed to [standard out](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). In production, it's recommended to be set to `false`. By default, JSON encoded logs are produced. Debug enables colored, human-readable, and more verbose logs to be streamed to [standard out](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). In production, it's recommended to be set to `false`.
For example, if true. For example, if `true`.
``` ```
10:37AM INF cmd/pomerium version=v0.0.1-dirty+ede4124 10:37AM INF cmd/pomerium version=v0.0.1-dirty+ede4124
@ -60,7 +71,7 @@ For example, if true.
10:37AM INF proxy/authenticator: grpc connection OverrideCertificateName= addr=auth.corp.beyondperimeter.com:443 10:37AM INF proxy/authenticator: grpc connection OverrideCertificateName= addr=auth.corp.beyondperimeter.com:443
``` ```
If false: If `false`:
``` ```
{"level":"info","version":"v0.0.1-dirty+ede4124","time":"2019-02-18T10:41:03-08:00","message":"cmd/pomerium"} {"level":"info","version":"v0.0.1-dirty+ede4124","time":"2019-02-18T10:41:03-08:00","message":"cmd/pomerium"}
@ -96,21 +107,6 @@ Certificate key is the x509 _private-key_ used to establish secure HTTP and gRPC
Redirect URL is the url the user will be redirected to following authentication with the third-party identity provider (IdP). Note the URL ends with `/oauth2/callback`. This setting will mirror the URL set when configuring your [identity provider]. Typically, on the provider side, this is called an _authorized callback url_. Redirect URL is the url the user will be redirected to following authentication with the third-party identity provider (IdP). Note the URL ends with `/oauth2/callback`. This setting will mirror the URL set when configuring your [identity provider]. Typically, on the provider side, this is called an _authorized callback url_.
### Allowed Email Domains
::: warning
This setting will be deprecated with the upcoming release of the authorization service.
:::
- Environmental Variable: `ALLOWED_DOMAINS`
- Type: `[]string` (e.g. comma separated list of strings)
- Required
- Example: `engineering.corp-b.com,devops.corp-a.com`
The allowed email domains settings dictates which email domains are valid for access. If an authenticated user has a email ending in a non-whitelisted domain, the request will be denied as unauthorized.
### Proxy Root Domains ### Proxy Root Domains
- Environmental Variable: `PROXY_ROOT_DOMAIN` - Environmental Variable: `PROXY_ROOT_DOMAIN`
@ -126,7 +122,7 @@ Proxy Root Domains specifies the sub-domains that can proxy requests. For exampl
- Required - Required
- Options: `azure` `google` `okta` `gitlab` `onelogin` or `oidc` - Options: `azure` `google` `okta` `gitlab` `onelogin` or `oidc`
Provider is the short-hand name of a built-in OpenID Connect (oidc) identity provider to be used for authentication. To use a generic provider,set to `oidc`. Provider is the short-hand name of a built-in OpenID Connect (oidc) identity provider to be used for authentication. To use a generic provider,set to `oidc`.
See [identity provider] for details. See [identity provider] for details.
@ -161,19 +157,18 @@ Provider URL is the base path to an identity provider's [OpenID connect discover
- Default: `oidc`,`profile`, `email`, `offline_access` (typically) - Default: `oidc`,`profile`, `email`, `offline_access` (typically)
- Optional for built-in identity providers. - Optional for built-in identity providers.
Identity provider scopes correspond to access privilege scopes as defined in Section 3.3 of OAuth 2.0 RFC6749. The scopes associated with Access Tokens determine what resources will be available when they are used to access OAuth 2.0 protected endpoints. If you are using a built-in provider, you probably don't want to set customized scopes. Identity provider scopes correspond to access privilege scopes as defined in Section 3.3 of OAuth 2.0 RFC6749\. The scopes associated with Access Tokens determine what resources will be available when they are used to access OAuth 2.0 protected endpoints. If you are using a built-in provider, you probably don't want to set customized scopes.
### Identity Provider Service Account
- Environmental Variable: `IDP_SERVICE_ACCOUNT`
- Type: `string`
- Required, depending on provider
Identity Provider Service Account is field used to configure any additional user account or access-token that may be required for querying additional user information during authentication. For a concrete example, Google an additional service account and to make a follow-up request to query a user's group membership. For more information, refer to the [identity provider] docs to see if your provider requires this setting.
## Proxy Service ## Proxy Service
### Routes
- Environmental Variable: `ROUTES`
- Type: `map[string]string` comma separated mapping of managed entities.
- Required
- Example: `https://httpbin.corp.example.com=http://httpbin,https://hello.corp.example.com=http://hello:8080/`
The routes setting contains a mapping of routes to be managed by pomerium.
### Signing Key ### Signing Key
- Environmental Variable: `SIGNING_KEY` - Environmental Variable: `SIGNING_KEY`
@ -198,17 +193,25 @@ Authenticate Service URL is the externally accessible URL for the authenticate s
- Optional - Optional
- Example: `pomerium-authenticate-service.pomerium.svc.cluster.local` - Example: `pomerium-authenticate-service.pomerium.svc.cluster.local`
Authenticate Internal Service URL is the internal location of the authenticate service. This setting is used to override the authenticate service url for when you need to do "behind-the-ingress" inter-service communication. This is typically required for ingresses and load balancers that do not support HTTP/2 or gRPC termination. Authenticate Internal Service URL is the internally routed dns name of the authenticate service. This setting is used to override the authenticate service url for when you need to do "behind-the-ingress" inter-service communication. This is typically required for ingresses and load balancers that do not support HTTP/2 or gRPC termination.
### Authenticate Service Port ### Authorize Service URL
- Environmental Variable: `AUTHENTICATE_SERVICE_PORT` - Environmental Variable: `AUTHORIZE_SERVICE_URL`
- Type: `int` - Type: `URL`
- Required
- Example: `https://access.corp.example.com`
Authorize Service URL is the externally accessible URL for the authorize service.
### Authorize Internal Service URL
- Environmental Variable: `AUTHORIZE_INTERNAL_URL`
- Type: `string`
- Optional - Optional
- Default: `443` - Example: `pomerium-authorize-service.pomerium.svc.cluster.local`
- Example: `8443`
Authenticate Service Port is used to set the port value for authenticate service communication. Authorize Internal Service URL is the internally routed dns name of the authorize service. This setting is used to override the authorize service url for when you need to do "behind-the-ingress" inter-service communication. This is typically required for ingresses and load balancers that do not support HTTP/2 or gRPC termination.
### Override Certificate Name ### Override Certificate Name
@ -217,7 +220,7 @@ Authenticate Service Port is used to set the port value for authenticate service
- Optional (but typically required if Authenticate Internal Service Address is set) - Optional (but typically required if Authenticate Internal Service Address is set)
- Example: `*.corp.example.com` if wild card or `authenticate.corp.example.com` - Example: `*.corp.example.com` if wild card or `authenticate.corp.example.com`
When Authenticate Internal Service Address is set, secure service communication can fail because the external certificate name will not match the internally routed service url. This setting allows you to override that check. When Authenticate Internal Service Address is set, secure service communication can fail because the external certificate name will not match the internally routed service url. This setting allows you to override that check.
### Certificate Authority ### Certificate Authority

View file

@ -56,20 +56,6 @@ Customize for your identity provider run `docker-compose up -f nginx.docker-comp
<<< @/docs/docs/examples/docker/nginx.docker-compose.yml <<< @/docs/docs/examples/docker/nginx.docker-compose.yml
### Gitlab On-Prem
- Docker and Docker-Compose based.
- Uses pre-configured built-in nginx load balancer
- Runs separate containers for each service
- Comes with a pre-configured instance of on-prem Gitlab-CE
- Routes default to on-prem [helloworld], [httpbin], and [gitlab].
Customize for your identity provider run `docker-compose up -f gitlab.docker-compose.yml`
#### gitlab.docker-compose.yml
<<< @/docs/docs/examples/docker/gitlab.docker-compose.yml
## Kubernetes ## Kubernetes
### Google Kubernetes Engine ### Google Kubernetes Engine
@ -103,7 +89,6 @@ Customize for your identity provider run `docker-compose up -f gitlab.docker-com
<<< @/docs/docs/examples/kubernetes/ingress.yml <<< @/docs/docs/examples/kubernetes/ingress.yml
[gitlab]: https://docs.gitlab.com/ee/user/project/container_registry.html
[helloworld]: https://hub.docker.com/r/tutum/hello-world [helloworld]: https://hub.docker.com/r/tutum/hello-world
[httpbin]: https://httpbin.org/ [httpbin]: https://httpbin.org/
[https load balancing]: https://cloud.google.com/kubernetes-engine/docs/concepts/ingress [https load balancing]: https://cloud.google.com/kubernetes-engine/docs/concepts/ingress

View file

@ -1,43 +1,35 @@
# Example Pomerium configuration. # Example Pomerium configuration.
# #
# NOTE! Change IDP_* settings to match your identity provider settings! # NOTE! Change IDP_* settings to match your identity provider settings!
# NOTE! Generate new SHARED_SECRET and COOKIE_SECRET keys! # NOTE! Generate new SHARED_SECRET and COOKIE_SECRET keys! e.g. `head -c32 /dev/urandom | base64`
# NOTE! Replace `corp.beyondperimeter.com` with whatever your domain is # NOTE! Replace `corp.beyondperimeter.com` with whatever your domain is
# NOTE! Make sure certificate files (cert.pem/privkey.pem) are in the same directory as this file # NOTE! Make sure certificate files (cert.pem/privkey.pem) are in the same directory as this file
# NOTE! Wrap URLs in quotes to avoid parse errors # NOTE! Make sure your policy file (policy.example.yaml) is in the same directory as this file
version: "3" version: "3"
services: services:
pomerium-all: pomerium:
image: pomerium/pomerium:latest # or `build: .` to build from source image: pomerium/pomerium:latest # or `build: .` to build from source
environment: environment:
- POMERIUM_DEBUG=true
- SERVICES=all - SERVICES=all
# auth settings
- REDIRECT_URL=https://auth.corp.beyondperimeter.com/oauth2/callback - REDIRECT_URL=https://auth.corp.beyondperimeter.com/oauth2/callback
# Identity Provider Settings (Must be changed!)
- IDP_PROVIDER=google - IDP_PROVIDER=google
- IDP_PROVIDER_URL=https://accounts.google.com - IDP_PROVIDER_URL=https://accounts.google.com
- IDP_CLIENT_ID=REPLACE_ME.apps.googleusercontent.com - IDP_CLIENT_ID=REPLACE_ME.apps.googleusercontent.com
- IDP_CLIENT_SECRET=REPLACE_ME - IDP_CLIENT_SECRET=REPLACE_ME
# - SCOPE="openid email"
- PROXY_ROOT_DOMAIN=beyondperimeter.com - PROXY_ROOT_DOMAIN=beyondperimeter.com
- ALLOWED_DOMAINS=*
# shared service settings
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M= - SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI= - COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
# proxy settings - CERTIFICATE_FILE=cert.pem
- CERTIFICATE_KEY_FILE=privkey.pem
- AUTHENTICATE_SERVICE_URL=https://auth.corp.beyondperimeter.com - AUTHENTICATE_SERVICE_URL=https://auth.corp.beyondperimeter.com
- ROUTES=https://httpbin.corp.beyondperimeter.com=http://httpbin,https://helloworld.corp.beyondperimeter.com=http://helloworld:8080/ - AUTHORIZE_SERVICE_URL=https://access.corp.beyondperimeter.com
# - SIGNING_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo= - POLICY_FILE=./policy.yaml
# if passing certs as files volumes:
# - CERTIFICATE_KEY=corp.beyondperimeter.com.crt
# - CERTIFICATE_KEY_FILE=corp.beyondperimeter.com.key
# Or, you can pass certifcates as bas64 encoded values. e.g. `base64 -i cert.pem`
# - CERTIFICATE=
# - CERTIFICATE_KEY=
volumes: # volumes is optional; used if passing certificates as files
- ./cert.pem:/pomerium/cert.pem:ro - ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro - ./privkey.pem:/pomerium/privkey.pem:ro
- ./policy.example.yaml:/pomerium/policy.yaml:ro
ports: ports:
- 443:443 - 443:443
@ -46,9 +38,8 @@ services:
image: kennethreitz/httpbin:latest image: kennethreitz/httpbin:latest
expose: expose:
- 80 - 80
# https://hello.corp.beyondperimeter.com
# https://helloworld.corp.beyondperimeter.com hello:
helloworld:
image: gcr.io/google-samples/hello-app:1.0 image: gcr.io/google-samples/hello-app:1.0
expose: expose:
- 8080 - 8080

View file

@ -1,94 +0,0 @@
version: "3"
services:
nginx:
image: pomerium/nginx-proxy:latest
ports:
- "443:443"
volumes:
# NOTE!!! : nginx must be supplied with your wildcard certificates. And it expects
# it in the format of whatever your wildcard domain name is in.
# see : https://github.com/jwilder/nginx-proxy#wildcard-certificates
# So, if your subdomain is corp.beyondperimeter.com, you'd have the following :
- ./cert.pem:/etc/nginx/certs/corp.beyondperimeter.com.crt:ro
- ./privkey.pem:/etc/nginx/certs/corp.beyondperimeter.com.key:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
pomerium-authenticate:
build: .
restart: always
environment:
- POMERIUM_DEBUG=true
- SERVICES=authenticate
# auth settings
- REDIRECT_URL=https://auth.corp.beyondperimeter.com/oauth2/callback
# Identity Provider Settings (Must be changed!)
- IDP_PROVIDER=google
- IDP_PROVIDER_URL=https://accounts.google.com
- IDP_CLIENT_ID=REPLACEME
- IDP_CLIENT_SECRET=REPLACE_ME
- PROXY_ROOT_DOMAIN=corp.beyondperimeter.com
- ALLOWED_DOMAINS=*
- SKIP_PROVIDER_BUTTON=false
# shared service settings
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=auth.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
expose:
- 443
pomerium-proxy:
build: .
restart: always
environment:
- POMERIUM_DEBUG=true
- SERVICES=proxy
# proxy settings
- AUTHENTICATE_SERVICE_URL=https://auth.corp.beyondperimeter.com
# IMPORTANT! If you are running pomerium behind another ingress (loadbalancer/firewall/etc)
# you must tell pomerium proxy how to communicate using an internal hostname for RPC
- AUTHENTICATE_INTERNAL_URL=pomerium-authenticate:443
# When communicating internally, rPC is going to get a name conflict expecting an external
# facing certificate name (i.e. authenticate-service.local vs *.corp.example.com).
- OVERRIDE_CERTIFICATE_NAME=*.corp.beyondperimeter.com
- ROUTES=https://gitlab.corp.beyondperimeter.com=https://gitlab
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
# nginx settings
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=*.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
expose:
- 443
gitlab:
hostname: gitlab.corp.beyondperimeter.com
image: gitlab/gitlab-ce:latest
restart: always
expose:
- 443
- 80
- 22
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'https://gitlab.corp.beyondperimeter.com'
nginx['ssl_certificate'] = '/etc/gitlab/trusted-certs/corp.beyondperimeter.com.crt'
nginx['ssl_certificate_key'] = '/etc/gitlab/trusted-certs/corp.beyondperimeter.com.key'
VIRTUAL_PROTO: https
VIRTUAL_HOST: gitlab.corp.beyondperimeter.com
VIRTUAL_PORT: 443
volumes:
- ./cert.pem:/etc/gitlab/trusted-certs/corp.beyondperimeter.com.crt
- ./privkey.pem:/etc/gitlab/trusted-certs/corp.beyondperimeter.com.key
- $HOME/gitlab/config:/etc/gitlab
- $HOME/gitlab/logs:/var/log/gitlab
- $HOME/gitlab/data:/var/opt/gitlab

View file

@ -15,59 +15,75 @@ services:
- /var/run/docker.sock:/tmp/docker.sock:ro - /var/run/docker.sock:/tmp/docker.sock:ro
pomerium-authenticate: pomerium-authenticate:
build: . image: pomerium/pomerium:latest # or `build: .` to build from source
restart: always restart: always
environment: environment:
- POMERIUM_DEBUG=true - POMERIUM_DEBUG=true
- SERVICES=authenticate - SERVICES=authenticate
# auth settings
- REDIRECT_URL=https://auth.corp.beyondperimeter.com/oauth2/callback - REDIRECT_URL=https://auth.corp.beyondperimeter.com/oauth2/callback
# Identity Provider Settings (Must be changed!) # Identity Provider Settings (Must be changed!)
- IDP_PROVIDER=google - IDP_PROVIDER=google
- IDP_PROVIDER_URL=https://accounts.google.com - IDP_PROVIDER_URL=https://accounts.google.com
- IDP_CLIENT_ID=REPLACEME - IDP_CLIENT_ID=REPLACE_ME.apps.googleusercontent.com
- IDP_CLIENT_SECRET=REPLACE_ME - IDP_CLIENT_SECRET=REPLACE_ME
- PROXY_ROOT_DOMAIN=corp.beyondperimeter.com - PROXY_ROOT_DOMAIN=corp.beyondperimeter.com
- ALLOWED_DOMAINS=*
- SKIP_PROVIDER_BUTTON=false
# shared service settings
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M= - SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI= - COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
# nginx settings
- VIRTUAL_PROTO=https - VIRTUAL_PROTO=https
- VIRTUAL_HOST=auth.corp.beyondperimeter.com - VIRTUAL_HOST=auth.corp.beyondperimeter.com
- VIRTUAL_PORT=443 - VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files volumes:
- ./cert.pem:/pomerium/cert.pem:ro - ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro - ./privkey.pem:/pomerium/privkey.pem:ro
expose: expose:
- 443 - 443
pomerium-proxy:
build: .
restart: always
pomerium-proxy:
image: pomerium/pomerium:latest # or `build: .` to build from source
restart: always
environment: environment:
- POMERIUM_DEBUG=true - POMERIUM_DEBUG=true
- SERVICES=proxy - SERVICES=proxy
# proxy settings - POLICY_FILE=policy.yaml
- AUTHENTICATE_SERVICE_URL=https://auth.corp.beyondperimeter.com - AUTHENTICATE_SERVICE_URL=https://auth.corp.beyondperimeter.com
- AUTHORIZE_SERVICE_URL=https://access.corp.beyondperimeter.com
# IMPORTANT! If you are running pomerium behind another ingress (loadbalancer/firewall/etc) # IMPORTANT! If you are running pomerium behind another ingress (loadbalancer/firewall/etc)
# you must tell pomerium proxy how to communicate using an internal hostname for RPC # you must tell pomerium proxy how to communicate using an internal hostname for RPC
- AUTHENTICATE_INTERNAL_URL=pomerium-authenticate:443 - AUTHENTICATE_INTERNAL_URL=pomerium-authenticate:443
- AUTHORIZE_INTERNAL_URL=pomerium-authorize:443
# When communicating internally, rPC is going to get a name conflict expecting an external # When communicating internally, rPC is going to get a name conflict expecting an external
# facing certificate name (i.e. authenticate-service.local vs *.corp.example.com). # facing certificate name (i.e. authenticate-service.local vs *.corp.example.com).
- OVERRIDE_CERTIFICATE_NAME=*.corp.beyondperimeter.com - OVERRIDE_CERTIFICATE_NAME=*.corp.beyondperimeter.com
- ROUTES=https://httpbin.corp.beyondperimeter.com=http://httpbin,https://hello.corp.beyondperimeter.com=http://hello:8080/
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M= - SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI= - COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
# nginx settings # nginx settings
- VIRTUAL_PROTO=https - VIRTUAL_PROTO=https
- VIRTUAL_HOST=*.corp.beyondperimeter.com - VIRTUAL_HOST=*.corp.beyondperimeter.com
- VIRTUAL_PORT=443 - VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files volumes:
- ./cert.pem:/pomerium/cert.pem:ro - ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro - ./privkey.pem:/pomerium/privkey.pem:ro
- ./policy.example.yaml:/pomerium/policy.yaml:ro
expose:
- 443
pomerium-authorize:
image: pomerium/pomerium:latest # or `build: .` to build from source
restart: always
environment:
- POMERIUM_DEBUG=true
- SERVICES=authorize
- POLICY_FILE=policy.yaml
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
# nginx settings
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=access.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes:
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
- ./policy.example.yaml:/pomerium/policy.yaml:ro
expose: expose:
- 443 - 443

View file

@ -8,6 +8,7 @@ metadata:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true" nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k" nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
# to avoid ingress routing, enable
# nginx.ingress.kubernetes.io/ssl-passthrough: "true" # nginx.ingress.kubernetes.io/ssl-passthrough: "true"
spec: spec:
@ -16,6 +17,8 @@ spec:
hosts: hosts:
- "*.corp.beyondperimeter.com" - "*.corp.beyondperimeter.com"
- "auth.corp.beyondperimeter.com" - "auth.corp.beyondperimeter.com"
- "access.corp.beyondperimeter.com"
rules: rules:
- host: "*.corp.beyondperimeter.com" - host: "*.corp.beyondperimeter.com"
http: http:
@ -32,3 +35,10 @@ spec:
backend: backend:
serviceName: pomerium-authenticate-service serviceName: pomerium-authenticate-service
servicePort: https servicePort: https
- host: "access.corp.beyondperimeter.com"
http:
paths:
- paths:
backend:
serviceName: pomerium-authorize-service
servicePort: https

View file

@ -13,6 +13,8 @@ spec:
hosts: hosts:
- "*.corp.beyondperimeter.com" - "*.corp.beyondperimeter.com"
- "auth.corp.beyondperimeter.com" - "auth.corp.beyondperimeter.com"
- "access.corp.beyondperimeter.com"
rules: rules:
- host: "*.corp.beyondperimeter.com" - host: "*.corp.beyondperimeter.com"
http: http:
@ -29,3 +31,10 @@ spec:
backend: backend:
serviceName: pomerium-authenticate-service serviceName: pomerium-authenticate-service
servicePort: https servicePort: https
- host: "access.corp.beyondperimeter.com"
http:
paths:
- paths:
backend:
serviceName: pomerium-authorize-service
servicePort: https

View file

@ -23,10 +23,12 @@ spec:
name: https name: https
protocol: TCP protocol: TCP
env: env:
- name: ROUTES
value: https://httpbin.corp.beyondperimeter.com=https://httpbin.org,https://hi.corp.beyondperimeter.com=http://hello-app.pomerium.svc.cluster.local:8080
- name: SERVICES - name: SERVICES
value: proxy value: proxy
- name: AUTHORIZE_SERVICE_URL
value: https://access.corp.beyondperimeter.com
- name: AUTHORIZE_INTERNAL_URL
value: "pomerium-authorize-service.pomerium.svc.cluster.local"
- name: AUTHENTICATE_SERVICE_URL - name: AUTHENTICATE_SERVICE_URL
value: https://auth.corp.beyondperimeter.com value: https://auth.corp.beyondperimeter.com
- name: AUTHENTICATE_INTERNAL_URL - name: AUTHENTICATE_INTERNAL_URL

View file

@ -120,7 +120,7 @@ IDP_CLIENT_SECRET="REPLACE-ME"
:::warning :::warning
Support was removed in v0.0.3 because Gitlab does not provide callers with a user email, under any scope, to a caller unless that user has selected her email to be public. Pomerium until [this gitlab bug](https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387) is fixed. Support was removed in v0.0.3 because Gitlab does not provide callers with a user email, under any scope, to a caller unless that user has selected her email to be public. Pomerium support is blocked until [this gitlab bug](https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387) is fixed.
::: :::
@ -192,14 +192,16 @@ In order to have Pomerium validate group membership, we'll also need to configur
![Google create service account](./google/google-create-sa.png) ![Google create service account](./google/google-create-sa.png)
Then, you'll need to manually open an editor add an `impersonate_user` key to the downloaded public/private key file. In this case, we'd be impersonating the admin account `user@pomerium.io`. Then, you'll need to manually open an editor and add an `impersonate_user` field to the downloaded public/private key file. In this case, we'd be impersonating the admin account `user@pomerium.io`.
::: warning ::: warning
You MUST add the `impersonate_user` field to your json key file. [Google requires](https://stackoverflow.com/questions/48585700/is-it-possible-to-call-apis-from-service-account-without-acting-on-behalf-of-a-u/48601364#48601364) that service accounts act on behalf of another user. [Google requires](https://stackoverflow.com/questions/48585700/is-it-possible-to-call-apis-from-service-account-without-acting-on-behalf-of-a-u/48601364#48601364) that service accounts act on behalf of another user. You MUST add the `impersonate_user` field to your json key file.
::: :::
```json ```json
{ {
"type": "service_account", "type": "service_account",
@ -210,7 +212,7 @@ You MUST add the `impersonate_user` field to your json key file. [Google require
} }
``` ```
The base64 encoded contents of this public/private key pair json file will used for the value of the `IDP_SERVICE_ACCOUNT` configuration setting. The base64 encoded contents of this public/private key pair json file will used for the value of the `IDP_SERVICE_ACCOUNT` configuration setting.
Next we'll delegate G-suite group membership access to the service account we just created . Next we'll delegate G-suite group membership access to the service account we just created .

View file

@ -6,7 +6,7 @@ Pomerium is an open-source, identity-aware access proxy.
## Why ## Why
Traditional [perimeter](https://www.redbooks.ibm.com/redpapers/pdfs/redp4397.pdf) [security](https://en.wikipedia.org/wiki/Perimeter_Security)has some shortcomings, namely: Traditional [perimeter](https://www.redbooks.ibm.com/redpapers/pdfs/redp4397.pdf) [security](https://en.wikipedia.org/wiki/Perimeter_Security) has some shortcomings, namely:
- Insider threat is not well addressed and 28% of breaches are [by internal actors](http://www.documentwereld.nl/files/2018/Verizon-DBIR_2018-Main_report.pdf). - Insider threat is not well addressed and 28% of breaches are [by internal actors](http://www.documentwereld.nl/files/2018/Verizon-DBIR_2018-Main_report.pdf).
- Impenetrable fortress in theory falls in practice; multiple entry points (like VPNs), lots of firewall rules, network segmentation creep. - Impenetrable fortress in theory falls in practice; multiple entry points (like VPNs), lots of firewall rules, network segmentation creep.

View file

@ -27,17 +27,19 @@ The command will run all the tests, some code linters, then build the binary. If
## Configure ## Configure
Make a copy of the [env.example] and name it something like `env`. ### Environmental Configuration Variables
```bash Create a environmental configuration file modify its configuration to to match your [identity provider] settings. For example, `env`:
cp env.example env
```
Modify your `env` configuration to to match your [identity provider] settings. <<< @/env.example
```bash
vim env ### policy.yaml
``` Next, create a policy configuration file which will contain the routes you want to proxy, and their desired access-controls. For example, `policy.example.yaml`:
<<< @/policy.example.yaml
### Certificates
Place your domain's wild-card TLS certificate next to the compose file. If you don't have one handy, the included [script] generates one from [LetsEncrypt]. Place your domain's wild-card TLS certificate next to the compose file. If you don't have one handy, the included [script] generates one from [LetsEncrypt].

View file

@ -35,6 +35,8 @@ git clone https://github.com/pomerium/pomerium.git $HOME/pomerium
Edit the the [example kubernetes files][./scripts/kubernetes_gke.sh] to match your [identity provider] settings: Edit the the [example kubernetes files][./scripts/kubernetes_gke.sh] to match your [identity provider] settings:
- `./docs/docs/examples/authorize.deploy.yml`
- `./docs/docs/examples/authorize.service.yml`
- `./docs/docs/examples/authenticate.deploy.yml` - `./docs/docs/examples/authenticate.deploy.yml`
- `./docs/docs/examples/authenticate.service.yml` - `./docs/docs/examples/authenticate.service.yml`
- `./docs/docs/examples/proxy.deploy.yml` - `./docs/docs/examples/proxy.deploy.yml`
@ -50,8 +52,8 @@ Edit [./scripts/kubernetes_gke.sh] making sure to change the identity provider s
Run [./scripts/kubernetes_gke.sh] which will: Run [./scripts/kubernetes_gke.sh] which will:
1. Provision a new cluster 1. Provision a new cluster
2. Create authenticate and proxy [deployments](https://cloud.google.com/kubernetes-engine/docs/concepts/deployment). 2. Create authenticate, authorize, and proxy [deployments](https://cloud.google.com/kubernetes-engine/docs/concepts/deployment).
3. Provision and apply authenticate and proxy [services](https://cloud.google.com/kubernetes-engine/docs/concepts/service). 3. Provision and apply authenticate, authorize, and proxy [services](https://cloud.google.com/kubernetes-engine/docs/concepts/service).
4. Configure an ingress load balancer. 4. Configure an ingress load balancer.
```bash ```bash

View file

@ -10,11 +10,23 @@ Docker and docker-compose are tools for defining and running multi-container Doc
## Download ## Download
Copy and paste the contents of the provided example [basic.docker-compose.yml] and save it locally as `docker-compose.yml`. Copy and paste the contents of the provided example [basic.docker-compose.yml].
## Configure ## Configure
Edit the `docker-compose.yml` to match your [identity provider] settings. ### Docker-compose
Edit the `docker-compose.yml` to match your specific [identity provider]'s settings. For example, `basic.docker-compose.yml`:
<<< @/docs/docs/examples/docker/basic.docker-compose.yml
### Policy configuration
Next, create a policy configuration file which will contain the routes you want to proxy, and their desired access-controls. For example, `policy.example.yaml`:
<<< @/policy.example.yaml
### Certificates
Place your domain's wild-card TLS certificate next to the compose file. If you don't have one handy, the included [script] generates one from [LetsEncrypt]. Place your domain's wild-card TLS certificate next to the compose file. If you don't have one handy, the included [script] generates one from [LetsEncrypt].

View file

@ -42,6 +42,11 @@ export IDP_PROVIDER_URL="https://accounts.google.com" # optional for google
export IDP_CLIENT_ID="REPLACE-ME.googleusercontent.com" export IDP_CLIENT_ID="REPLACE-ME.googleusercontent.com"
export IDP_CLIENT_SECRET="REPLACEME" export IDP_CLIENT_SECRET="REPLACEME"
# IF GSUITE and you want to get user groups you will need to set a service account
# see identity provider docs for gooogle for more info :
# GSUITE_JSON_SERVICE_ACCOUNT='{"impersonate_user": "bdd@pomerium.io"}'
# export IDP_SERVICE_ACCOUNT=$(echo $GSUITE_JSON_SERVICE_ACCOUNT | base64)
# OKTA # OKTA
# export IDP_PROVIDER="okta" # export IDP_PROVIDER="okta"
# export IDP_CLIENT_ID="REPLACEME" # export IDP_CLIENT_ID="REPLACEME"
@ -56,8 +61,7 @@ export IDP_CLIENT_SECRET="REPLACEME"
# export SCOPE="openid email" # generally, you want the default OIDC scopes # export SCOPE="openid email" # generally, you want the default OIDC scopes
# k/v seperated list of simple routes. If no scheme is set, HTTPS will be used. # Proxied routes and per-route policies are defined in a policy provided either
# Currently set to httpbin which is a handy utility letting you inspect requests recieved by # directly as a base64 encoded yaml/json file, or as a path pointing to a
# a client application # policy file (`POLICY_FILE`)
export ROUTES="httpbin.corp.example.com=httpbin.org" export POLICY_FILE="./policy.example.yml"
# export ROUTES="https://weirdlyssl.corp.example.com=http://neverssl.com" #https to http!

7
go.mod
View file

@ -5,14 +5,19 @@ go 1.12
require ( require (
github.com/golang/mock v1.2.0 github.com/golang/mock v1.2.0
github.com/golang/protobuf v1.3.0 github.com/golang/protobuf v1.3.0
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31 github.com/google/pprof v0.0.0-20190228041337-2ef8d84b2e3c // indirect
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 // indirect
github.com/pomerium/envconfig v1.4.0
github.com/pomerium/go-oidc v2.0.0+incompatible github.com/pomerium/go-oidc v2.0.0+incompatible
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/rs/zerolog v1.12.0 github.com/rs/zerolog v1.12.0
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/arch v0.0.0-20190226203302-36aee92af9e8 // indirect
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7 golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
google.golang.org/api v0.1.0 google.golang.org/api v0.1.0
google.golang.org/grpc v1.19.0 google.golang.org/grpc v1.19.0
gopkg.in/square/go-jose.v2 v2.3.0 gopkg.in/square/go-jose.v2 v2.3.0
gopkg.in/yaml.v2 v2.2.2
) )

22
go.sum
View file

@ -1,10 +1,14 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@ -14,14 +18,22 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/pprof v0.0.0-20190228041337-2ef8d84b2e3c h1:hqIMb/MbwYamune8FA5YtFAVzfTE8OXRtg9Nf0rzmqo=
github.com/google/pprof v0.0.0-20190228041337-2ef8d84b2e3c/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pomerium/envconfig v1.3.0 h1:/qJ+JOrWKkd/MgSrBDQ6xYJ7sxzqxiIAB/3qgHwdrHY= github.com/pomerium/envconfig v1.3.0 h1:/qJ+JOrWKkd/MgSrBDQ6xYJ7sxzqxiIAB/3qgHwdrHY=
github.com/pomerium/envconfig v1.3.0/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc= github.com/pomerium/envconfig v1.3.0/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc=
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31 h1:bNqUesLWa+RUxQvSaV3//dEFviXdCSvMF9GKDOopFLU= github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31 h1:bNqUesLWa+RUxQvSaV3//dEFviXdCSvMF9GKDOopFLU=
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc= github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc=
github.com/pomerium/envconfig v1.4.0 h1:o+WY/E/9M4fh0nDX7oJodU7N9p1hcHPsTnNLYjlbQA8=
github.com/pomerium/envconfig v1.4.0/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc=
github.com/pomerium/go-oidc v2.0.0+incompatible h1:gVvG/ExWsHQqatV+uceROnGmbVYF44mDNx5nayBhC0o= github.com/pomerium/go-oidc v2.0.0+incompatible h1:gVvG/ExWsHQqatV+uceROnGmbVYF44mDNx5nayBhC0o=
github.com/pomerium/go-oidc v2.0.0+incompatible/go.mod h1:DRsGVw6MOgxbfq4Y57jKOE8lbEfayxeiY0A8/4vxjBM= github.com/pomerium/go-oidc v2.0.0+incompatible/go.mod h1:DRsGVw6MOgxbfq4Y57jKOE8lbEfayxeiY0A8/4vxjBM=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
@ -32,7 +44,12 @@ github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7q
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rs/zerolog v1.12.0 h1:aqZ1XRadoS8IBknR5IDFvGzbHly1X9ApIqOroooQF/c= github.com/rs/zerolog v1.12.0 h1:aqZ1XRadoS8IBknR5IDFvGzbHly1X9ApIqOroooQF/c=
github.com/rs/zerolog v1.12.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.12.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
golang.org/x/arch v0.0.0-20190226203302-36aee92af9e8 h1:G3kY3WDPiChidkYzLqbniw7jg23paUtzceZorG6YAJw=
golang.org/x/arch v0.0.0-20190226203302-36aee92af9e8/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -50,6 +67,7 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFM
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -64,6 +82,7 @@ google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -74,9 +93,12 @@ google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9M
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U= gopkg.in/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -11,6 +11,7 @@ import (
) )
func TestPasswordHashing(t *testing.T) { func TestPasswordHashing(t *testing.T) {
t.Parallel()
bcryptTests := []struct { bcryptTests := []struct {
plaintext []byte plaintext []byte
hash []byte hash []byte

View file

@ -25,27 +25,6 @@ nFUSTwqQFo5gbfIlP+gvEYba+Rxj2hhqjfzqxIleRK40IRyEi3fJM/8Qhg==
-----END PUBLIC KEY----- -----END PUBLIC KEY-----
` `
// A keypair for NIST P-384 / secp384r1
// Generated using:
// openssl ecparam -genkey -name secp384r1 -outform PEM
var pemECPrivateKeyP384 = `-----BEGIN EC PARAMETERS-----
BgUrgQQAIg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDAhA0YPVL1kimIy+FAqzUAtmR3It2Yjv2I++YpcC4oX7wGuEWcWKBYE
oOjj7wG/memgBwYFK4EEACKhZANiAAQub8xaaCTTW5rCHJCqUddIXpvq/TxdwViH
+tPEQQlJAJciXStM/aNLYA7Q1K1zMjYyzKSWz5kAh/+x4rXQ9Hlm3VAwCQDVVSjP
bfiNOXKOWfmyrGyQ7fQfs+ro1lmjLjs=
-----END EC PRIVATE KEY-----
`
var pemECPublicKeyP384 = `-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAELm/MWmgk01uawhyQqlHXSF6b6v08XcFY
h/rTxEEJSQCXIl0rTP2jS2AO0NStczI2Msykls+ZAIf/seK10PR5Zt1QMAkA1VUo
z234jTlyjln5sqxskO30H7Pq6NZZoy47
-----END PUBLIC KEY-----
`
var garbagePEM = `-----BEGIN GARBAGE----- var garbagePEM = `-----BEGIN GARBAGE-----
TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ= TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ=
-----END GARBAGE----- -----END GARBAGE-----

View file

@ -22,7 +22,7 @@ func TestES256Signer(t *testing.T) {
} }
func TestNewES256Signer(t *testing.T) { func TestNewES256Signer(t *testing.T) {
t.Parallel()
tests := []struct { tests := []struct {
name string name string
privKey []byte privKey []byte

View file

@ -4,6 +4,7 @@ import (
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
stdlog "log"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -12,6 +13,7 @@ import (
"time" "time"
"github.com/pomerium/pomerium/internal/fileutil" "github.com/pomerium/pomerium/internal/fileutil"
"github.com/pomerium/pomerium/internal/log"
"google.golang.org/grpc" "google.golang.org/grpc"
) )
@ -90,15 +92,18 @@ func ListenAndServeTLS(opt *Options, httpHandler http.Handler, grpcHandler *grpc
} else { } else {
h = grpcHandlerFunc(grpcHandler, httpHandler) h = grpcHandlerFunc(grpcHandler, httpHandler)
} }
sublogger := log.With().Str("addr", opt.Addr).Logger()
// Set up the main server. // Set up the main server.
server := &http.Server{ server := &http.Server{
ReadHeaderTimeout: 5 * time.Second, ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second,
// WriteTimeout is set to 0 for streaming replies // WriteTimeout is set to 0 for streaming replies
WriteTimeout: 0, WriteTimeout: 0,
IdleTimeout: 60 * time.Second, IdleTimeout: 5 * time.Minute,
TLSConfig: config, TLSConfig: config,
Handler: h, Handler: h,
ErrorLog: stdlog.New(&log.StdLogWrapper{Logger: &sublogger}, "", 0),
} }
return server.Serve(ln) return server.Serve(ln)

View file

@ -44,8 +44,10 @@ func NewGoogleProvider(p *Provider) (*GoogleProvider, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Google rejects the offline scope favoring "access_type=offline"
// as part of the authorization request instead.
if len(p.Scopes) == 0 { if len(p.Scopes) == 0 {
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "offline_access"} p.Scopes = []string{oidc.ScopeOpenID, "profile", "email"}
} }
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID}) p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{ p.oauth = &oauth2.Config{
@ -86,7 +88,7 @@ func NewGoogleProvider(p *Provider) (*GoogleProvider, error) {
return nil, fmt.Errorf("identity/google: failed creating admin service %v", err) return nil, fmt.Errorf("identity/google: failed creating admin service %v", err)
} }
} else { } else {
log.Warn().Msg("identity/google: no service account found, cannot retrieve user groups") log.Warn().Msg("identity/google: no service account, cannot retrieve groups")
} }
gp.RevokeURL, err = url.Parse(claims.RevokeURL) gp.RevokeURL, err = url.Parse(claims.RevokeURL)
@ -114,6 +116,11 @@ func (p *GoogleProvider) Revoke(accessToken string) error {
// the required scopes explicitly. // the required scopes explicitly.
// Google requires an additional access scope for offline access which is a requirement for any // Google requires an additional access scope for offline access which is a requirement for any
// application that needs to access a Google API when the user is not present. // application that needs to access a Google API when the user is not present.
// Support for this scope differs between OpenID Connect providers. For instance
// Google rejects it, favoring appending "access_type=offline" as part of the
// authorization request instead.
//
// https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
// https://developers.google.com/identity/protocols/OAuth2WebServer#offline // https://developers.google.com/identity/protocols/OAuth2WebServer#offline
func (p *GoogleProvider) GetSignInURL(state string) string { func (p *GoogleProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce) return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
@ -167,7 +174,7 @@ func (p *GoogleProvider) Authenticate(code string) (*sessions.SessionState, erro
}, nil }, nil
} }
// Refresh renews a user's session using an oid refresh token without reprompting the user. // Refresh renews a user's session using an oid refresh token withoutreprompting the user.
// Group membership is also refreshed. // Group membership is also refreshed.
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens // https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
func (p *GoogleProvider) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) { func (p *GoogleProvider) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) {
@ -211,7 +218,6 @@ func (p *GoogleProvider) UserGroups(ctx context.Context, user string) ([]string,
return nil, fmt.Errorf("identity/google: group api request failed %v", err) return nil, fmt.Errorf("identity/google: group api request failed %v", err)
} }
for _, group := range resp.Groups { for _, group := range resp.Groups {
log.Info().Str("group.Name", group.Name).Msg("sanity check3")
groups = append(groups, group.Name) groups = append(groups, group.Name)
} }
} }

View file

@ -1,5 +1,3 @@
//go:generate protoc -I ../../proto/authenticate --go_out=plugins=grpc:../../proto/authenticate ../../proto/authenticate/authenticate.proto
// Package identity provides support for making OpenID Connect and OAuth2 authorized and // Package identity provides support for making OpenID Connect and OAuth2 authorized and
// authenticated HTTP requests with third party identity providers. // authenticated HTTP requests with third party identity providers.
package identity // import "github.com/pomerium/pomerium/internal/identity" package identity // import "github.com/pomerium/pomerium/internal/identity"

View file

@ -110,3 +110,19 @@ func Ctx(ctx context.Context) *zerolog.Logger {
func FromRequest(r *http.Request) *zerolog.Logger { func FromRequest(r *http.Request) *zerolog.Logger {
return Ctx(r.Context()) return Ctx(r.Context())
} }
// StdLogWrapper can be used to wrap logs originating from the from std
// library's ErrorFunction argument in http.Serve and httputil.ReverseProxy.
type StdLogWrapper struct {
*zerolog.Logger
}
func (l *StdLogWrapper) Write(p []byte) (n int, err error) {
n = len(p)
if n > 0 && p[n-1] == '\n' {
// Trim CR added by stdlog.
p = p[0 : n-1]
}
l.Error().Msg(string(p))
return len(p), nil
}

View file

@ -70,7 +70,7 @@ func TestThenFuncTreatsNilAsDefaultServeMux(t *testing.T) {
func TestThenFuncConstructsHandlerFunc(t *testing.T) { func TestThenFuncConstructsHandlerFunc(t *testing.T) {
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200) w.WriteHeader(http.StatusOK)
}) })
chained := NewChain().ThenFunc(fn) chained := NewChain().ThenFunc(fn)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()

83
internal/policy/policy.go Normal file
View file

@ -0,0 +1,83 @@
package policy // import "github.com/pomerium/pomerium/internal/policy"
import (
"fmt"
"io/ioutil"
"net/url"
"strings"
"time"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/fileutil"
yaml "gopkg.in/yaml.v2"
)
// Policy contains authorization policy information.
// todo(bdd) : add upstream timeout and configuration settings
type Policy struct {
// proxy related
From string `yaml:"from"`
To string `yaml:"to"`
// upstream transport settings
UpstreamTimeout time.Duration `yaml:"timeout"`
// Identity related policy
AllowedEmails []string `yaml:"allowed_users"`
AllowedGroups []string `yaml:"allowed_groups"`
AllowedDomains []string `yaml:"allowed_domains"`
Source *url.URL
Destination *url.URL
}
func (p *Policy) validate() (err error) {
p.Source, err = urlParse(p.From)
if err != nil {
return err
}
p.Destination, err = urlParse(p.To)
if err != nil {
return err
}
return nil
}
// FromConfig parses configuration file as bytes and returns authorization
// policies. Supports yaml, json.
func FromConfig(confBytes []byte) ([]Policy, error) {
var f []Policy
if err := yaml.Unmarshal(confBytes, &f); err != nil {
return nil, err
}
// build source and destination urls
for i := range f {
if err := (&f[i]).validate(); err != nil {
return nil, err
}
}
log.Info().Msgf("from config %+v", f)
return f, nil
}
// FromConfigFile parses configuration file from a path and returns
// authorization policies. Supports yaml, json.
func FromConfigFile(f string) ([]Policy, error) {
exists, err := fileutil.IsReadableFile(f)
if err != nil || !exists {
return nil, fmt.Errorf("policy file %v: %v exists? %v", f, err, exists)
}
confBytes, err := ioutil.ReadFile(f)
if err != nil {
return nil, err
}
return FromConfig(confBytes)
}
// urlParse wraps url.Parse to add a scheme if none-exists.
// https://github.com/golang/go/issues/12585
func urlParse(uri string) (*url.URL, error) {
if !strings.Contains(uri, "://") {
uri = fmt.Sprintf("https://%s", uri)
}
return url.ParseRequestURI(uri)
}

View file

@ -0,0 +1,92 @@
package policy
import (
"net/url"
"reflect"
"testing"
)
func TestFromConfig(t *testing.T) {
t.Parallel()
source, _ := urlParse("pomerium.io")
dest, _ := urlParse("httpbin.org")
tests := []struct {
name string
yamlBytes []byte
want []Policy
wantErr bool
}{
{"simple json", []byte(`[{"from": "pomerium.io","to":"httpbin.org"}]`), []Policy{{From: "pomerium.io", To: "httpbin.org", Source: source, Destination: dest}}, false},
{"bad from", []byte(`[{"from": "%","to":"httpbin.org"}]`), nil, true},
{"bad to", []byte(`[{"from": "pomerium.io","to":"%"}]`), nil, true},
{"simple error", []byte(`{}`), nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := FromConfig(tt.yamlBytes)
if (err != nil) != tt.wantErr {
t.Errorf("FromConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("FromConfig() = \n%v, want \n%v", got, tt.want)
}
})
}
}
func Test_urlParse(t *testing.T) {
t.Parallel()
tests := []struct {
name string
uri string
want *url.URL
wantErr bool
}{
{"good url without schema", "accounts.google.com", &url.URL{Scheme: "https", Host: "accounts.google.com"}, false},
{"good url with schema", "https://accounts.google.com", &url.URL{Scheme: "https", Host: "accounts.google.com"}, false},
{"bad url, malformed", "https://accounts.google.^", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := urlParse(tt.uri)
if (err != nil) != tt.wantErr {
t.Errorf("urlParse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("urlParse() = %v, want %v", got, tt.want)
}
})
}
}
func TestFromConfigFile(t *testing.T) {
t.Parallel()
source, _ := urlParse("pomerium.io")
dest, _ := urlParse("httpbin.org")
tests := []struct {
name string
f string
want []Policy
wantErr bool
}{
{"simple json", "./testdata/basic.json", []Policy{{From: "pomerium.io", To: "httpbin.org", Source: source, Destination: dest}}, false},
{"simple yaml", "./testdata/basic.yaml", []Policy{{From: "pomerium.io", To: "httpbin.org", Source: source, Destination: dest}}, false},
{"failed dir", "./testdata/", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := FromConfigFile(tt.f)
if (err != nil) != tt.wantErr {
t.Errorf("FromConfigFile() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("FromConfigFile() = %v, want %v", got, tt.want)
}
})
}
}

6
internal/policy/testdata/basic.json vendored Normal file
View file

@ -0,0 +1,6 @@
[
{
"from": "pomerium.io",
"to": "httpbin.org"
}
]

2
internal/policy/testdata/basic.yaml vendored Normal file
View file

@ -0,0 +1,2 @@
- from: pomerium.io
to: httpbin.org

View file

@ -106,17 +106,6 @@ func TestCookieStore_makeCookie(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
type fields struct {
Name string
CSRFCookieName string
CookieCipher cryptutil.Cipher
CookieExpire time.Duration
CookieRefresh time.Duration
CookieSecure bool
CookieHTTPOnly bool
CookieDomain string
}
now := time.Now() now := time.Now()
tests := []struct { tests := []struct {
name string name string

19
policy.example.yaml Normal file
View file

@ -0,0 +1,19 @@
- from: httpbin.corp.beyondperimeter.com
to: http://httpbin
allowed_domains:
- pomerium.io
- from: external-httpbin.corp.beyondperimeter.com
to: httpbin.org
allowed_domains:
- gmail.com
- from: weirdlyssl.corp.beyondperimeter.com
to: http://neverssl.com
allowed_users:
- bdd@pomerium.io
allowed_groups:
- admins
- developers
- from: hello.corp.beyondperimeter.com
to: http://hello:8080
allowed_groups:
- admins

View file

@ -1,142 +0,0 @@
package mock_authenticate_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
pb "github.com/pomerium/pomerium/proto/authenticate"
mock "github.com/pomerium/pomerium/proto/authenticate/mock_authenticate"
)
var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
// rpcMsg implements the gomock.Matcher interface
type rpcMsg struct {
msg proto.Message
}
func (r *rpcMsg) Matches(msg interface{}) bool {
m, ok := msg.(proto.Message)
if !ok {
return false
}
return proto.Equal(m, r.msg)
}
func (r *rpcMsg) String() string {
return fmt.Sprintf("is %s", r.msg)
}
func TestValidate(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockAuthenticateClient := mock.NewMockAuthenticatorClient(ctrl)
req := &pb.ValidateRequest{IdToken: "unit_test"}
mockAuthenticateClient.EXPECT().Validate(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.ValidateReply{IsValid: false}, nil)
testValidate(t, mockAuthenticateClient)
}
func testValidate(t *testing.T, client pb.AuthenticatorClient) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := client.Validate(ctx, &pb.ValidateRequest{IdToken: "unit_test"})
if err != nil || r.IsValid != false {
t.Errorf("mocking failed")
}
t.Log("Reply : ", r.IsValid)
}
func TestAuthenticate(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockAuthenticateClient := mock.NewMockAuthenticatorClient(ctrl)
mockExpire, err := ptypes.TimestampProto(fixedDate)
if err != nil {
t.Fatalf("%v failed converting timestamp", err)
}
req := &pb.AuthenticateRequest{Code: "unit_test"}
mockAuthenticateClient.EXPECT().Authenticate(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.Session{
AccessToken: "mocked access token",
RefreshToken: "mocked refresh token",
IdToken: "mocked id token",
User: "user1",
Email: "test@email.com",
LifetimeDeadline: mockExpire,
}, nil)
testAuthenticate(t, mockAuthenticateClient)
}
func testAuthenticate(t *testing.T, client pb.AuthenticatorClient) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := client.Authenticate(ctx, &pb.AuthenticateRequest{Code: "unit_test"})
if err != nil {
t.Errorf("mocking failed %v", err)
}
if r.AccessToken != "mocked access token" {
t.Errorf("authenticate: invalid access token")
}
if r.RefreshToken != "mocked refresh token" {
t.Errorf("authenticate: invalid refresh token")
}
if r.IdToken != "mocked id token" {
t.Errorf("authenticate: invalid id token")
}
if r.User != "user1" {
t.Errorf("authenticate: invalid user")
}
if r.Email != "test@email.com" {
t.Errorf("authenticate: invalid email")
}
}
func TestRefresh(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRefreshClient := mock.NewMockAuthenticatorClient(ctrl)
mockExpire, err := ptypes.TimestampProto(fixedDate)
if err != nil {
t.Fatalf("%v failed converting timestamp", err)
}
req := &pb.Session{RefreshToken: "unit_test"}
mockRefreshClient.EXPECT().Refresh(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.Session{
AccessToken: "mocked access token",
LifetimeDeadline: mockExpire,
}, nil)
testRefresh(t, mockRefreshClient)
}
func testRefresh(t *testing.T, client pb.AuthenticatorClient) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := client.Refresh(ctx, &pb.Session{RefreshToken: "unit_test"})
if err != nil {
t.Errorf("mocking failed %v", err)
}
if r.AccessToken != "mocked access token" {
t.Errorf("Refresh: invalid access token")
}
respExpire, err := ptypes.Timestamp(r.LifetimeDeadline)
if err != nil {
t.Fatalf("%v failed converting timestamp", err)
}
if respExpire != fixedDate {
t.Errorf("Refresh: bad expiry got:%v want:%v", respExpire, fixedDate)
}
}

View file

@ -0,0 +1,221 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: authorize.proto
package authorize
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type AuthorizeRequest struct {
// request context
Route string `protobuf:"bytes,1,opt,name=route,proto3" json:"route,omitempty"`
// user context
User string `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"`
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
Groups []string `protobuf:"bytes,4,rep,name=groups,proto3" json:"groups,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *AuthorizeRequest) Reset() { *m = AuthorizeRequest{} }
func (m *AuthorizeRequest) String() string { return proto.CompactTextString(m) }
func (*AuthorizeRequest) ProtoMessage() {}
func (*AuthorizeRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_authorize_dad4e29706fc340b, []int{0}
}
func (m *AuthorizeRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_AuthorizeRequest.Unmarshal(m, b)
}
func (m *AuthorizeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_AuthorizeRequest.Marshal(b, m, deterministic)
}
func (dst *AuthorizeRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_AuthorizeRequest.Merge(dst, src)
}
func (m *AuthorizeRequest) XXX_Size() int {
return xxx_messageInfo_AuthorizeRequest.Size(m)
}
func (m *AuthorizeRequest) XXX_DiscardUnknown() {
xxx_messageInfo_AuthorizeRequest.DiscardUnknown(m)
}
var xxx_messageInfo_AuthorizeRequest proto.InternalMessageInfo
func (m *AuthorizeRequest) GetRoute() string {
if m != nil {
return m.Route
}
return ""
}
func (m *AuthorizeRequest) GetUser() string {
if m != nil {
return m.User
}
return ""
}
func (m *AuthorizeRequest) GetEmail() string {
if m != nil {
return m.Email
}
return ""
}
func (m *AuthorizeRequest) GetGroups() []string {
if m != nil {
return m.Groups
}
return nil
}
type AuthorizeReply struct {
IsValid bool `protobuf:"varint,1,opt,name=is_valid,json=isValid,proto3" json:"is_valid,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *AuthorizeReply) Reset() { *m = AuthorizeReply{} }
func (m *AuthorizeReply) String() string { return proto.CompactTextString(m) }
func (*AuthorizeReply) ProtoMessage() {}
func (*AuthorizeReply) Descriptor() ([]byte, []int) {
return fileDescriptor_authorize_dad4e29706fc340b, []int{1}
}
func (m *AuthorizeReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_AuthorizeReply.Unmarshal(m, b)
}
func (m *AuthorizeReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_AuthorizeReply.Marshal(b, m, deterministic)
}
func (dst *AuthorizeReply) XXX_Merge(src proto.Message) {
xxx_messageInfo_AuthorizeReply.Merge(dst, src)
}
func (m *AuthorizeReply) XXX_Size() int {
return xxx_messageInfo_AuthorizeReply.Size(m)
}
func (m *AuthorizeReply) XXX_DiscardUnknown() {
xxx_messageInfo_AuthorizeReply.DiscardUnknown(m)
}
var xxx_messageInfo_AuthorizeReply proto.InternalMessageInfo
func (m *AuthorizeReply) GetIsValid() bool {
if m != nil {
return m.IsValid
}
return false
}
func init() {
proto.RegisterType((*AuthorizeRequest)(nil), "authorize.AuthorizeRequest")
proto.RegisterType((*AuthorizeReply)(nil), "authorize.AuthorizeReply")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// AuthorizerClient is the client API for Authorizer service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type AuthorizerClient interface {
Authorize(ctx context.Context, in *AuthorizeRequest, opts ...grpc.CallOption) (*AuthorizeReply, error)
}
type authorizerClient struct {
cc *grpc.ClientConn
}
func NewAuthorizerClient(cc *grpc.ClientConn) AuthorizerClient {
return &authorizerClient{cc}
}
func (c *authorizerClient) Authorize(ctx context.Context, in *AuthorizeRequest, opts ...grpc.CallOption) (*AuthorizeReply, error) {
out := new(AuthorizeReply)
err := c.cc.Invoke(ctx, "/authorize.Authorizer/Authorize", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// AuthorizerServer is the server API for Authorizer service.
type AuthorizerServer interface {
Authorize(context.Context, *AuthorizeRequest) (*AuthorizeReply, error)
}
func RegisterAuthorizerServer(s *grpc.Server, srv AuthorizerServer) {
s.RegisterService(&_Authorizer_serviceDesc, srv)
}
func _Authorizer_Authorize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AuthorizeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthorizerServer).Authorize(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/authorize.Authorizer/Authorize",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthorizerServer).Authorize(ctx, req.(*AuthorizeRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Authorizer_serviceDesc = grpc.ServiceDesc{
ServiceName: "authorize.Authorizer",
HandlerType: (*AuthorizerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Authorize",
Handler: _Authorizer_Authorize_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "authorize.proto",
}
func init() { proto.RegisterFile("authorize.proto", fileDescriptor_authorize_dad4e29706fc340b) }
var fileDescriptor_authorize_dad4e29706fc340b = []byte{
// 187 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4f, 0x2c, 0x2d, 0xc9,
0xc8, 0x2f, 0xca, 0xac, 0x4a, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x84, 0x0b, 0x28,
0x65, 0x71, 0x09, 0x38, 0xc2, 0x38, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x22, 0x5c,
0xac, 0x45, 0xf9, 0xa5, 0x25, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x10, 0x8e, 0x90,
0x10, 0x17, 0x4b, 0x69, 0x71, 0x6a, 0x91, 0x04, 0x13, 0x58, 0x10, 0xcc, 0x06, 0xa9, 0x4c, 0xcd,
0x4d, 0xcc, 0xcc, 0x91, 0x60, 0x86, 0xa8, 0x04, 0x73, 0x84, 0xc4, 0xb8, 0xd8, 0xd2, 0x8b, 0xf2,
0x4b, 0x0b, 0x8a, 0x25, 0x58, 0x14, 0x98, 0x35, 0x38, 0x83, 0xa0, 0x3c, 0x25, 0x6d, 0x2e, 0x3e,
0x24, 0xbb, 0x0a, 0x72, 0x2a, 0x85, 0x24, 0xb9, 0x38, 0x32, 0x8b, 0xe3, 0xcb, 0x12, 0x73, 0x32,
0x53, 0xc0, 0x96, 0x71, 0x04, 0xb1, 0x67, 0x16, 0x87, 0x81, 0xb8, 0x46, 0xc1, 0x5c, 0x5c, 0x70,
0xc5, 0x45, 0x42, 0xae, 0x5c, 0x9c, 0x70, 0x9e, 0x90, 0xb4, 0x1e, 0xc2, 0x43, 0xe8, 0x8e, 0x97,
0x92, 0xc4, 0x2e, 0x59, 0x90, 0x53, 0xa9, 0xc4, 0x90, 0xc4, 0x06, 0xf6, 0xbf, 0x31, 0x20, 0x00,
0x00, 0xff, 0xff, 0x28, 0xac, 0x76, 0x2d, 0x12, 0x01, 0x00, 0x00,
}

View file

@ -0,0 +1,18 @@
syntax = "proto3";
package authorize;
service Authorizer {
rpc Authorize(AuthorizeRequest) returns (AuthorizeReply) {}
}
message AuthorizeRequest {
// request context
string route = 1;
// user context
string user = 2;
string email = 3;
repeated string groups = 4;
}
message AuthorizeReply { bool is_valid = 1; }

View file

@ -1,45 +0,0 @@
package authenticator // import "github.com/pomerium/pomerium/proxy/authenticator"
import (
"context"
"github.com/pomerium/pomerium/internal/sessions"
)
// Authenticator provides the authenticate service interface
type Authenticator interface {
// Redeem takes a code and returns a validated session or an error
Redeem(context.Context, string) (*sessions.SessionState, error)
// Refresh attempts to refresh a valid session with a refresh token. Returns a refreshed session.
Refresh(context.Context, *sessions.SessionState) (*sessions.SessionState, error)
// Validate evaluates a given oidc id_token for validity. Returns validity and any error.
Validate(context.Context, string) (bool, error)
// Close closes the authenticator connection if any.
Close() error
}
// Options contains options for connecting to an authenticate service .
type Options struct {
// Addr is the location of the authenticate service. Used if InternalAddr is not set.
Addr string
Port int
// InternalAddr is the internal (behind the ingress) address to use when making an
// authentication connection. If empty, Addr is used.
InternalAddr string
// OverrideCertificateName overrides the server name used to verify the hostname on the
// returned certificates from the server. gRPC internals also use it to override the virtual
// hosting name if it is set.
OverrideCertificateName string
// Shared secret is used to authenticate a authenticate-client with a authenticate-server.
SharedSecret string
// CA specifies the base64 encoded TLS certificate authority to use.
CA string
// CAFile specifies the TLS certificate authority file to use.
CAFile string
}
// New returns a new authenticate service client. Takes a client implementation name as an argument.
// Currently only gRPC is supported and is always returned.
func New(name string, opts *Options) (a Authenticator, err error) {
return NewGRPC(opts)
}

View file

@ -1,90 +1,37 @@
package authenticator // import "github.com/pomerium/pomerium/proxy/authenticator" package clients // import "github.com/pomerium/pomerium/proxy/clients"
import ( import (
"context" "context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors" "errors"
"fmt"
"io/ioutil"
"strings"
"time" "time"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
pb "github.com/pomerium/pomerium/proto/authenticate" pb "github.com/pomerium/pomerium/proto/authenticate"
) )
// NewGRPC returns a new authenticate service client. // Authenticator provides the authenticate service interface
func NewGRPC(opts *Options) (p *AuthenticateGRPC, err error) { type Authenticator interface {
// gRPC uses a pre-shared secret middleware to establish authentication b/w server and client // Redeem takes a code and returns a validated session or an error
if opts.SharedSecret == "" { Redeem(context.Context, string) (*sessions.SessionState, error)
return nil, errors.New("proxy/authenticator: grpc client requires shared secret") // Refresh attempts to refresh a valid session with a refresh token. Returns a refreshed session.
} Refresh(context.Context, *sessions.SessionState) (*sessions.SessionState, error)
grpcAuth := middleware.NewSharedSecretCred(opts.SharedSecret) // Validate evaluates a given oidc id_token for validity. Returns validity and any error.
Validate(context.Context, string) (bool, error)
// Close closes the authenticator connection if any.
Close() error
}
var connAddr string // NewAuthenticateClient returns a new authenticate service client.
if opts.InternalAddr != "" { func NewAuthenticateClient(name string, opts *Options) (a Authenticator, err error) {
connAddr = opts.InternalAddr // Only gRPC is supported and is always returned so name is ignored
} else { return NewGRPCAuthenticateClient(opts)
connAddr = opts.Addr }
}
if connAddr == "" {
return nil, errors.New("proxy/authenticator: connection address required")
}
// no colon exists in the connection string, assume one must be added manually
if !strings.Contains(connAddr, ":") {
connAddr = fmt.Sprintf("%s:%d", connAddr, opts.Port)
}
var cp *x509.CertPool // NewGRPCAuthenticateClient returns a new authenticate service client.
if opts.CA != "" || opts.CAFile != "" { func NewGRPCAuthenticateClient(opts *Options) (p *AuthenticateGRPC, err error) {
cp = x509.NewCertPool() conn, err := NewGRPCClientConn(opts)
var ca []byte
var err error
if opts.CA != "" {
ca, err = base64.StdEncoding.DecodeString(opts.CA)
if err != nil {
return nil, fmt.Errorf("failed to decode certificate authority: %v", err)
}
} else {
ca, err = ioutil.ReadFile(opts.CAFile)
if err != nil {
return nil, fmt.Errorf("certificate authority file %v not readable: %v", opts.CAFile, err)
}
}
if ok := cp.AppendCertsFromPEM(ca); !ok {
return nil, fmt.Errorf("failed to append CA cert to certPool")
}
} else {
newCp, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
cp = newCp
}
log.Info().
Str("OverrideCertificateName", opts.OverrideCertificateName).
Str("addr", connAddr).Msgf("proxy/authenticator: grpc connection")
cert := credentials.NewTLS(&tls.Config{RootCAs: cp})
// override allowed certificate name string, typically used when doing behind ingress connection
if opts.OverrideCertificateName != "" {
err = cert.OverrideServerName(opts.OverrideCertificateName)
if err != nil {
return nil, err
}
}
conn, err := grpc.Dial(
connAddr,
grpc.WithTransportCredentials(cert),
grpc.WithPerRPCCredentials(grpcAuth),
)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -1,4 +1,4 @@
package authenticator package clients // import "github.com/pomerium/pomerium/proxy/clients"
import ( import (
"context" "context"
@ -16,6 +16,27 @@ import (
mock "github.com/pomerium/pomerium/proto/authenticate/mock_authenticate" mock "github.com/pomerium/pomerium/proto/authenticate/mock_authenticate"
) )
func TestNew(t *testing.T) {
tests := []struct {
name string
serviceName string
opts *Options
wantErr bool
}{
{"grpc good", "grpc", &Options{Addr: "test", InternalAddr: "intranet.local", SharedSecret: "secret"}, false},
{"grpc missing shared secret", "grpc", &Options{Addr: "test", InternalAddr: "intranet.local", SharedSecret: ""}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewAuthenticateClient(tt.serviceName, tt.opts)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC) var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
// rpcMsg implements the gomock.Matcher interface // rpcMsg implements the gomock.Matcher interface
@ -194,27 +215,27 @@ func TestNewGRPC(t *testing.T) {
wantTarget string wantTarget string
}{ }{
{"no shared secret", &Options{}, true, "proxy/authenticator: grpc client requires shared secret", ""}, {"no shared secret", &Options{}, true, "proxy/authenticator: grpc client requires shared secret", ""},
{"empty connection", &Options{Addr: "", Port: 443, SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""}, {"empty connection", &Options{Addr: "", SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""},
{"both internal and addr empty", &Options{Addr: "", Port: 443, InternalAddr: "", SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""}, {"both internal and addr empty", &Options{Addr: "", InternalAddr: "", SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""},
{"internal addr with port", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local:8443", SharedSecret: "shh"}, false, "", "intranet.local:8443"}, {"internal addr with port", &Options{Addr: "", InternalAddr: "intranet.local:8443", SharedSecret: "shh"}, false, "", "intranet.local:8443"},
{"internal addr without port", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", SharedSecret: "shh"}, false, "", "intranet.local:443"}, {"internal addr without port", &Options{Addr: "", InternalAddr: "intranet.local", SharedSecret: "shh"}, false, "", "intranet.local:443"},
{"cert override", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh"}, false, "", "intranet.local:443"}, {"cert override", &Options{Addr: "", InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh"}, false, "", "intranet.local:443"},
{"custom ca", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZrQ0ZBWHhneFg5K0hjWlBVVVBEK0laV0NGNUEvVTdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1FVXgKQ3pBSkJnTlZCQVlUQWtGVk1STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbApjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3SGhjTk1Ua3dNakk0TVRnMU1EQTNXaGNOTWprd01qSTFNVGcxCk1EQTNXakJGTVFzd0NRWURWUVFHRXdKQlZURVRNQkVHQTFVRUNBd0tVMjl0WlMxVGRHRjBaVEVoTUI4R0ExVUUKQ2d3WVNXNTBaWEp1WlhRZ1YybGtaMmwwY3lCUWRIa2dUSFJrTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBOVRFMEFiaTdnMHhYeURkVUtEbDViNTBCT05ZVVVSc3F2THQrSWkwdlpjMzRRTHhOClJrT0hrOFZEVUgzcUt1N2UrNGVubUdLVVNUdzRPNFlkQktiSWRJTFpnb3o0YitNL3FVOG5adVpiN2pBVTdOYWkKajMzVDVrbXB3L2d4WHNNUzNzdUpXUE1EUDB3Z1BUZUVRK2J1bUxVWmpLdUVIaWNTL0l5dmtaVlBzRlE4NWlaUwpkNXE2a0ZGUUdjWnFXeFg0dlhDV25Sd3E3cHY3TThJd1RYc1pYSVRuNXB5Z3VTczNKb29GQkg5U3ZNTjRKU25GCmJMK0t6ekduMy9ScXFrTXpMN3FUdkMrNWxVT3UxUmNES21mZXBuVGVaN1IyVnJUQm42NndWMjVHRnBkSDIzN00KOXhJVkJrWEd1U2NvWHVPN1lDcWFrZkt6aXdoRTV4UmRaa3gweXdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQQpBNElCQVFCaHRWUEI0OCs4eFZyVmRxM1BIY3k5QkxtVEtrRFl6N2Q0ODJzTG1HczBuVUdGSTFZUDdmaFJPV3ZxCktCTlpkNEI5MUpwU1NoRGUrMHpoNno4WG5Ha01mYnRSYWx0NHEwZ3lKdk9hUWhqQ3ZCcSswTFk5d2NLbXpFdnMKcTRiNUZ5NXNpRUZSekJLTmZtTGwxTTF2cW1hNmFCVnNYUUhPREdzYS83dE5MalZ2ay9PYm52cFg3UFhLa0E3cQpLMTQvV0tBRFBJWm9mb00xMzB4Q1RTYXVpeXROajlnWkx1WU9leEZhblVwNCt2MHBYWS81OFFSNTk2U0ROVTlKClJaeDhwTzBTaUYvZXkxVUZXbmpzdHBjbTQzTFVQKzFwU1hFeVhZOFJrRTI2QzNvdjNaTFNKc2pMbC90aXVqUlgKZUJPOWorWDdzS0R4amdtajBPbWdpVkpIM0YrUAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}, false, "", "intranet.local:443"}, {"custom ca", &Options{Addr: "", InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZrQ0ZBWHhneFg5K0hjWlBVVVBEK0laV0NGNUEvVTdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1FVXgKQ3pBSkJnTlZCQVlUQWtGVk1STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbApjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3SGhjTk1Ua3dNakk0TVRnMU1EQTNXaGNOTWprd01qSTFNVGcxCk1EQTNXakJGTVFzd0NRWURWUVFHRXdKQlZURVRNQkVHQTFVRUNBd0tVMjl0WlMxVGRHRjBaVEVoTUI4R0ExVUUKQ2d3WVNXNTBaWEp1WlhRZ1YybGtaMmwwY3lCUWRIa2dUSFJrTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBOVRFMEFiaTdnMHhYeURkVUtEbDViNTBCT05ZVVVSc3F2THQrSWkwdlpjMzRRTHhOClJrT0hrOFZEVUgzcUt1N2UrNGVubUdLVVNUdzRPNFlkQktiSWRJTFpnb3o0YitNL3FVOG5adVpiN2pBVTdOYWkKajMzVDVrbXB3L2d4WHNNUzNzdUpXUE1EUDB3Z1BUZUVRK2J1bUxVWmpLdUVIaWNTL0l5dmtaVlBzRlE4NWlaUwpkNXE2a0ZGUUdjWnFXeFg0dlhDV25Sd3E3cHY3TThJd1RYc1pYSVRuNXB5Z3VTczNKb29GQkg5U3ZNTjRKU25GCmJMK0t6ekduMy9ScXFrTXpMN3FUdkMrNWxVT3UxUmNES21mZXBuVGVaN1IyVnJUQm42NndWMjVHRnBkSDIzN00KOXhJVkJrWEd1U2NvWHVPN1lDcWFrZkt6aXdoRTV4UmRaa3gweXdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQQpBNElCQVFCaHRWUEI0OCs4eFZyVmRxM1BIY3k5QkxtVEtrRFl6N2Q0ODJzTG1HczBuVUdGSTFZUDdmaFJPV3ZxCktCTlpkNEI5MUpwU1NoRGUrMHpoNno4WG5Ha01mYnRSYWx0NHEwZ3lKdk9hUWhqQ3ZCcSswTFk5d2NLbXpFdnMKcTRiNUZ5NXNpRUZSekJLTmZtTGwxTTF2cW1hNmFCVnNYUUhPREdzYS83dE5MalZ2ay9PYm52cFg3UFhLa0E3cQpLMTQvV0tBRFBJWm9mb00xMzB4Q1RTYXVpeXROajlnWkx1WU9leEZhblVwNCt2MHBYWS81OFFSNTk2U0ROVTlKClJaeDhwTzBTaUYvZXkxVUZXbmpzdHBjbTQzTFVQKzFwU1hFeVhZOFJrRTI2QzNvdjNaTFNKc2pMbC90aXVqUlgKZUJPOWorWDdzS0R4amdtajBPbWdpVkpIM0YrUAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}, false, "", "intranet.local:443"},
{"bad ca encoding", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "^"}, true, "", "intranet.local:443"}, {"bad ca encoding", &Options{Addr: "", InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "^"}, true, "", "intranet.local:443"},
{"custom ca file", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt"}, false, "", "intranet.local:443"}, {"custom ca file", &Options{Addr: "", InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt"}, false, "", "intranet.local:443"},
{"bad custom ca file", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt2"}, true, "", "intranet.local:443"}, {"bad custom ca file", &Options{Addr: "", InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt2"}, true, "", "intranet.local:443"},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := NewGRPC(tt.opts) got, err := NewGRPCAuthenticateClient(tt.opts)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("NewGRPC() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("NewGRPCAuthenticateClient() error = %v, wantErr %v", err, tt.wantErr)
if !strings.EqualFold(err.Error(), tt.wantErrStr) { if !strings.EqualFold(err.Error(), tt.wantErrStr) {
t.Errorf("NewGRPC() error = %v did not contain wantErr %v", err, tt.wantErrStr) t.Errorf("NewGRPCAuthenticateClient() error = %v did not contain wantErr %v", err, tt.wantErrStr)
} }
} }
if got != nil && got.Conn.Target() != tt.wantTarget { if got != nil && got.Conn.Target() != tt.wantTarget {
t.Errorf("NewGRPC() target = %v expected %v", got.Conn.Target(), tt.wantTarget) t.Errorf("NewGRPCAuthenticateClient() target = %v expected %v", got.Conn.Target(), tt.wantTarget)
} }
}) })

View file

@ -0,0 +1,64 @@
package clients // import "github.com/pomerium/pomerium/proxy/clients"
import (
"context"
"errors"
"time"
pb "github.com/pomerium/pomerium/proto/authorize"
"google.golang.org/grpc"
"github.com/pomerium/pomerium/internal/sessions"
)
// Authorizer provides the authorize service interface
type Authorizer interface {
// Authorize takes a code and returns a validated session or an error
Authorize(context.Context, string, *sessions.SessionState) (bool, error)
// Close closes the auth connection if any.
Close() error
}
// NewAuthorizeClient returns a new authorize service client.
func NewAuthorizeClient(name string, opts *Options) (a Authorizer, err error) {
// Only gRPC is supported and is always returned so name is ignored
return NewGRPCAuthorizeClient(opts)
}
// NewGRPCAuthorizeClient returns a new authorize service client.
func NewGRPCAuthorizeClient(opts *Options) (p *AuthorizeGRPC, err error) {
conn, err := NewGRPCClientConn(opts)
if err != nil {
return nil, err
}
client := pb.NewAuthorizerClient(conn)
return &AuthorizeGRPC{Conn: conn, client: client}, nil
}
// AuthorizeGRPC is a gRPC implementation of an authenticator (authenticate client)
type AuthorizeGRPC struct {
Conn *grpc.ClientConn
client pb.AuthorizerClient
}
// Authorize makes an RPC call to the authorize service to creates a session state
// from an encrypted code provided as a result of an oauth2 callback process.
func (a *AuthorizeGRPC) Authorize(ctx context.Context, route string, s *sessions.SessionState) (bool, error) {
if s == nil {
return false, errors.New("session cannot be nil")
}
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
response, err := a.client.Authorize(ctx, &pb.AuthorizeRequest{
Route: route,
User: s.User,
Email: s.Email,
Groups: s.Groups,
})
return response.GetIsValid(), err
}
// Close tears down the ClientConn and all underlying connections.
func (a *AuthorizeGRPC) Close() error {
return a.Conn.Close()
}

View file

@ -0,0 +1,47 @@
package clients
import (
"context"
"testing"
"github.com/pomerium/pomerium/internal/sessions"
pb "github.com/pomerium/pomerium/proto/authorize"
"google.golang.org/grpc"
)
func TestAuthorizeGRPC_Authorize(t *testing.T) {
type fields struct {
Conn *grpc.ClientConn
client pb.AuthorizerClient
}
type args struct {
ctx context.Context
route string
s *sessions.SessionState
}
tests := []struct {
name string
fields fields
args args
want bool
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &AuthorizeGRPC{
Conn: tt.fields.Conn,
client: tt.fields.client,
}
got, err := a.Authorize(tt.args.ctx, tt.args.route, tt.args.s)
if (err != nil) != tt.wantErr {
t.Errorf("AuthorizeGRPC.Authorize() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("AuthorizeGRPC.Authorize() = %v, want %v", got, tt.want)
}
})
}
}

106
proxy/clients/clients.go Normal file
View file

@ -0,0 +1,106 @@
package clients // import "github.com/pomerium/pomerium/proxy/clients"
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
)
const defaultGRPCPort = 443
// Options contains options for connecting to a pomerium rpc service.
type Options struct {
// Addr is the location of the authenticate service. e.g. "service.corp.example:8443"
Addr string
// InternalAddr is the internal (behind the ingress) address to use when
// making a connection. If empty, Addr is used.
InternalAddr string
// OverrideCertificateName overrides the server name used to verify the hostname on the
// returned certificates from the server. gRPC internals also use it to override the virtual
// hosting name if it is set.
OverrideCertificateName string
// Shared secret is used to authenticate a authenticate-client with a authenticate-server.
SharedSecret string
// CA specifies the base64 encoded TLS certificate authority to use.
CA string
// CAFile specifies the TLS certificate authority file to use.
CAFile string
}
// NewGRPCClientConn returns a new gRPC pomerium service client connection.
func NewGRPCClientConn(opts *Options) (*grpc.ClientConn, error) {
// gRPC uses a pre-shared secret middleware to establish authentication b/w server and client
if opts.SharedSecret == "" {
return nil, errors.New("proxy/clients: grpc client requires shared secret")
}
grpcAuth := middleware.NewSharedSecretCred(opts.SharedSecret)
var connAddr string
if opts.InternalAddr != "" {
connAddr = opts.InternalAddr
} else {
connAddr = opts.Addr
}
if connAddr == "" {
return nil, errors.New("proxy/clients: connection address required")
}
// no colon exists in the connection string, assume one must be added manually
if !strings.Contains(connAddr, ":") {
connAddr = fmt.Sprintf("%s:%d", connAddr, defaultGRPCPort)
}
var cp *x509.CertPool
if opts.CA != "" || opts.CAFile != "" {
cp = x509.NewCertPool()
var ca []byte
var err error
if opts.CA != "" {
ca, err = base64.StdEncoding.DecodeString(opts.CA)
if err != nil {
return nil, fmt.Errorf("failed to decode certificate authority: %v", err)
}
} else {
ca, err = ioutil.ReadFile(opts.CAFile)
if err != nil {
return nil, fmt.Errorf("certificate authority file %v not readable: %v", opts.CAFile, err)
}
}
if ok := cp.AppendCertsFromPEM(ca); !ok {
return nil, fmt.Errorf("failed to append CA cert to certPool")
}
} else {
newCp, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
cp = newCp
}
log.Info().
Str("OverrideCertificateName", opts.OverrideCertificateName).
Str("addr", connAddr).Msgf("proxy/clients: grpc connection")
cert := credentials.NewTLS(&tls.Config{RootCAs: cp})
// override allowed certificate name string, typically used when doing behind ingress connection
if opts.OverrideCertificateName != "" {
err := cert.OverrideServerName(opts.OverrideCertificateName)
if err != nil {
return nil, err
}
}
return grpc.Dial(
connAddr,
grpc.WithTransportCredentials(cert),
grpc.WithPerRPCCredentials(grpcAuth),
)
}

View file

@ -1,4 +1,4 @@
package authenticator // import "github.com/pomerium/pomerium/proxy/authenticator" package clients // import "github.com/pomerium/pomerium/proxy/clients"
import ( import (
"context" "context"
@ -34,3 +34,18 @@ func (a MockAuthenticate) Validate(ctx context.Context, idToken string) (bool, e
// Close is a mocked authenticator client function. // Close is a mocked authenticator client function.
func (a MockAuthenticate) Close() error { return a.CloseError } func (a MockAuthenticate) Close() error { return a.CloseError }
// MockAuthorize provides a mocked implementation of the authorizer interface.
type MockAuthorize struct {
AuthorizeResponse bool
AuthorizeError error
CloseError error
}
// Close is a mocked authorizer client function.
func (a MockAuthorize) Close() error { return a.CloseError }
// Authorize is a mocked authorizer client function.
func (a MockAuthorize) Authorize(ctx context.Context, route string, s *sessions.SessionState) (bool, error) {
return a.AuthorizeResponse, a.AuthorizeError
}

View file

@ -1,4 +1,4 @@
package authenticator package clients
import ( import (
"context" "context"
@ -59,24 +59,3 @@ func TestMockAuthenticate(t *testing.T) {
} }
} }
func TestNew(t *testing.T) {
tests := []struct {
name string
serviceName string
opts *Options
wantErr bool
}{
{"grpc good", "grpc", &Options{Addr: "test", InternalAddr: "intranet.local", SharedSecret: "secret"}, false},
{"grpc missing shared secret", "grpc", &Options{Addr: "test", InternalAddr: "intranet.local", SharedSecret: ""}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := New(tt.serviceName, tt.opts)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}

View file

@ -19,7 +19,7 @@ import (
) )
var ( var (
// ErrUserNotAuthorized is set when user is not authorized to access a resource // ErrUserNotAuthorized is set when user is not authorized to access a resource.
ErrUserNotAuthorized = errors.New("user not authorized") ErrUserNotAuthorized = errors.New("user not authorized")
) )
@ -43,7 +43,7 @@ func (p *Proxy) Handler() http.Handler {
mux.HandleFunc("/robots.txt", p.RobotsTxt) mux.HandleFunc("/robots.txt", p.RobotsTxt)
mux.HandleFunc("/.pomerium/sign_out", p.SignOut) mux.HandleFunc("/.pomerium/sign_out", p.SignOut)
mux.HandleFunc("/.pomerium/callback", p.OAuthCallback) mux.HandleFunc("/.pomerium/callback", p.OAuthCallback)
// mux.HandleFunc("/.pomerium/refresh", p.Refresh) //todo(bdd): needs DoS protection before inclusion // mux.HandleFunc("/.pomerium/refresh", p.Refresh) //todo(bdd): needs DoS protection before inclusion
mux.HandleFunc("/", p.Proxy) mux.HandleFunc("/", p.Proxy)
// middleware chain // middleware chain
@ -242,9 +242,18 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
// remove dupe session call
// todo(bdd): add authorization service validation session, err := p.sessionStore.LoadSession(r)
if err != nil {
p.sessionStore.ClearSession(w, r)
return
}
authorized, err := p.AuthorizeClient.Authorize(r.Context(), r.Host, session)
if !authorized || err != nil {
log.FromRequest(r).Warn().Err(err).Msg("proxy: user unauthorized")
httputil.ErrorResponse(w, r, "Access unauthorized", http.StatusForbidden)
return
}
// We have validated the users request and now proxy their request to the provided upstream. // We have validated the users request and now proxy their request to the provided upstream.
route, ok := p.router(r) route, ok := p.router(r)
if !ok { if !ok {
@ -279,7 +288,7 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) // httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
// return // return
// } // }
// fmt.Fprintf(w, string(jsonSession)) // fmt.Fprint(w, string(jsonSession))
// } // }
// Authenticate authenticates a request by checking for a session cookie, and validating its expiration, // Authenticate authenticates a request by checking for a session cookie, and validating its expiration,

View file

@ -1,6 +1,7 @@
package proxy package proxy
import ( import (
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -13,7 +14,7 @@ import (
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version" "github.com/pomerium/pomerium/internal/version"
"github.com/pomerium/pomerium/proxy/authenticator" "github.com/pomerium/pomerium/proxy/clients"
) )
type mockCipher struct{} type mockCipher struct{}
@ -152,13 +153,11 @@ func TestProxy_Favicon(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
req := httptest.NewRequest("GET", "/favicon.ico", nil) req := httptest.NewRequest("GET", "/favicon.ico", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
proxy.Favicon(rr, req) proxy.Favicon(rr, req)
if status := rr.Code; status != http.StatusNotFound { if status := rr.Code; status != http.StatusNotFound {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound) t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound)
} }
// todo(bdd) : good way of mocking auth then serving a simple favicon?
} }
func TestProxy_Signout(t *testing.T) { func TestProxy_Signout(t *testing.T) {
@ -190,7 +189,7 @@ func TestProxy_OAuthStart(t *testing.T) {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusFound) t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusFound)
} }
// expected url // expected url
expected := `<a href="https://sso-auth.corp.beyondperimeter.com/sign_in` expected := `<a href="https://authenticate.corp.beyondperimeter.com/sign_in`
body := rr.Body.String() body := rr.Body.String()
if !strings.HasPrefix(body, expected) { if !strings.HasPrefix(body, expected) {
t.Errorf("handler returned unexpected body: got %v want %v", body, expected) t.Errorf("handler returned unexpected body: got %v want %v", body, expected)
@ -219,8 +218,6 @@ func TestProxy_Handler(t *testing.T) {
} }
func TestProxy_OAuthCallback(t *testing.T) { func TestProxy_OAuthCallback(t *testing.T) {
//todo(bdd): test malformed requests
// https://github.com/golang/go/blob/master/src/net/http/request_test.go#L110
normalSession := sessions.MockSessionStore{ normalSession := sessions.MockSessionStore{
Session: &sessions.SessionState{ Session: &sessions.SessionState{
AccessToken: "AccessToken", AccessToken: "AccessToken",
@ -229,7 +226,7 @@ func TestProxy_OAuthCallback(t *testing.T) {
RefreshDeadline: time.Now().Add(-10 * time.Second), RefreshDeadline: time.Now().Add(-10 * time.Second),
}, },
} }
normalAuth := authenticator.MockAuthenticate{ normalAuth := clients.MockAuthenticate{
RedeemResponse: &sessions.SessionState{ RedeemResponse: &sessions.SessionState{
AccessToken: "AccessToken", AccessToken: "AccessToken",
RefreshToken: "RefreshToken", RefreshToken: "RefreshToken",
@ -247,13 +244,13 @@ func TestProxy_OAuthCallback(t *testing.T) {
name string name string
csrf sessions.MockCSRFStore csrf sessions.MockCSRFStore
session sessions.MockSessionStore session sessions.MockSessionStore
authenticator authenticator.MockAuthenticate authenticator clients.MockAuthenticate
params map[string]string params map[string]string
wantCode int wantCode int
}{ }{
{"good", normalCsrf, normalSession, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusFound}, {"good", normalCsrf, normalSession, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusFound},
{"error", normalCsrf, normalSession, normalAuth, map[string]string{"error": "some error"}, http.StatusForbidden}, {"error", normalCsrf, normalSession, normalAuth, map[string]string{"error": "some error"}, http.StatusForbidden},
{"code err", normalCsrf, normalSession, authenticator.MockAuthenticate{RedeemError: errors.New("error")}, map[string]string{"code": "error"}, http.StatusInternalServerError}, {"code err", normalCsrf, normalSession, clients.MockAuthenticate{RedeemError: errors.New("error")}, map[string]string{"code": "error"}, http.StatusInternalServerError},
{"state err", normalCsrf, normalSession, normalAuth, map[string]string{"code": "code", "state": "error"}, http.StatusInternalServerError}, {"state err", normalCsrf, normalSession, normalAuth, map[string]string{"code": "code", "state": "error"}, http.StatusInternalServerError},
{"csrf err", sessions.MockCSRFStore{GetError: errors.New("error")}, normalSession, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusBadRequest}, {"csrf err", sessions.MockCSRFStore{GetError: errors.New("error")}, normalSession, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusBadRequest},
{"unmarshal err", sessions.MockCSRFStore{ {"unmarshal err", sessions.MockCSRFStore{
@ -311,30 +308,31 @@ func Test_extendDeadline(t *testing.T) {
} }
func TestProxy_router(t *testing.T) { func TestProxy_router(t *testing.T) {
configBlob := `[{"from":"corp.example.com","to":"example.com"}]` //valid yaml
policy := base64.URLEncoding.EncodeToString([]byte(configBlob))
tests := []struct { tests := []struct {
name string name string
host string host string
mux map[string]string mux string
route http.Handler route http.Handler
wantOk bool wantOk bool
}{ }{
{"good corp", "https://corp.example.com", map[string]string{"corp.example.com": "example.com"}, nil, true}, {"good corp", "https://corp.example.com", policy, nil, true},
{"good with slash", "https://corp.example.com/", map[string]string{"corp.example.com": "example.com"}, nil, true}, {"good with slash", "https://corp.example.com/", policy, nil, true},
{"good with path", "https://corp.example.com/123", map[string]string{"corp.example.com": "example.com"}, nil, true}, {"good with path", "https://corp.example.com/123", policy, nil, true},
{"multiple", "https://corp.example.com/", map[string]string{"corp.unrelated.com": "unrelated.com", "corp.example.com": "example.com"}, nil, true}, // {"multiple", "https://corp.example.com/", map[string]string{"corp.unrelated.com": "unrelated.com", "corp.example.com": "example.com"}, nil, true},
{"bad corp", "https://notcorp.example.com/123", map[string]string{"corp.example.com": "example.com"}, nil, false}, {"bad corp", "https://notcorp.example.com/123", policy, nil, false},
{"bad sub-sub", "https://notcorp.corp.example.com/123", map[string]string{"corp.example.com": "example.com"}, nil, false}, {"bad sub-sub", "https://notcorp.corp.example.com/123", policy, nil, false},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
opts := testOptions() opts := testOptions()
opts.Routes = tt.mux opts.Policy = tt.mux
p, err := New(opts) p, err := New(opts)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
p.AuthenticateClient = authenticator.MockAuthenticate{} p.AuthenticateClient = clients.MockAuthenticate{}
p.cipher = mockCipher{} p.cipher = mockCipher{}
req := httptest.NewRequest("GET", tt.host, nil) req := httptest.NewRequest("GET", tt.host, nil)
@ -358,14 +356,16 @@ func TestProxy_Proxy(t *testing.T) {
name string name string
host string host string
session sessions.SessionStore session sessions.SessionStore
authenticator authenticator.Authenticator authenticator clients.Authenticator
authorizer clients.Authorizer
wantStatus int wantStatus int
}{ }{
// weirdly, we want 503 here because that means proxy is trying to route a domain (example.com) that we dont control. Weird. I know. // weirdly, we want 503 here because that means proxy is trying to route a domain (example.com) that we dont control. Weird. I know.
{"good", "https://corp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, authenticator.MockAuthenticate{}, http.StatusServiceUnavailable}, {"good", "https://corp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusServiceUnavailable},
{"unexpected error", "https://corp.example.com/test", &sessions.MockSessionStore{LoadError: errors.New("ok")}, authenticator.MockAuthenticate{}, http.StatusInternalServerError}, {"unexpected error", "https://corp.example.com/test", &sessions.MockSessionStore{LoadError: errors.New("ok")}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusInternalServerError},
// redirect to start auth process // redirect to start auth process
{"unknown host", "https://notcorp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, authenticator.MockAuthenticate{}, http.StatusNotFound}, {"unknown host", "https://notcorp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound},
{"user forbidden", "https://notcorp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -377,6 +377,7 @@ func TestProxy_Proxy(t *testing.T) {
p.cipher = mockCipher{} p.cipher = mockCipher{}
p.sessionStore = tt.session p.sessionStore = tt.session
p.AuthenticateClient = tt.authenticator p.AuthenticateClient = tt.authenticator
p.AuthorizeClient = tt.authorizer
r := httptest.NewRequest("GET", tt.host, nil) r := httptest.NewRequest("GET", tt.host, nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -390,6 +391,8 @@ func TestProxy_Proxy(t *testing.T) {
} }
func TestProxy_Authenticate(t *testing.T) { func TestProxy_Authenticate(t *testing.T) {
configBlob := `[{"from":"corp.example.com","to":"example.com"}]` //valid yaml
policy := base64.URLEncoding.EncodeToString([]byte(configBlob))
goodSession := &sessions.SessionState{ goodSession := &sessions.SessionState{
User: "user", User: "user",
@ -400,21 +403,21 @@ func TestProxy_Authenticate(t *testing.T) {
LifetimeDeadline: time.Now().Add(10 * time.Second), LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(10 * time.Second), RefreshDeadline: time.Now().Add(10 * time.Second),
} }
testAuth := authenticator.MockAuthenticate{ testAuth := clients.MockAuthenticate{
RedeemResponse: goodSession, RedeemResponse: goodSession,
} }
tests := []struct { tests := []struct {
name string name string
host string host string
mux map[string]string mux string
session sessions.SessionStore session sessions.SessionStore
authenticator authenticator.Authenticator authenticator clients.Authenticator
wantErr bool wantErr bool
}{ }{
{"cannot save session", {"cannot save session",
"https://corp.example.com/", "https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"}, policy,
&sessions.MockSessionStore{Session: &sessions.SessionState{ &sessions.MockSessionStore{Session: &sessions.SessionState{
User: "user", User: "user",
Email: "email@email.com", Email: "email@email.com",
@ -428,11 +431,11 @@ func TestProxy_Authenticate(t *testing.T) {
{"cannot load session", {"cannot load session",
"https://corp.example.com/", "https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"}, policy,
&sessions.MockSessionStore{LoadError: errors.New("error")}, testAuth, true}, &sessions.MockSessionStore{LoadError: errors.New("error")}, testAuth, true},
{"expired session", {"expired session",
"https://corp.example.com/", "https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"}, policy,
&sessions.MockSessionStore{ &sessions.MockSessionStore{
Session: &sessions.SessionState{ Session: &sessions.SessionState{
User: "user", User: "user",
@ -443,7 +446,7 @@ func TestProxy_Authenticate(t *testing.T) {
LifetimeDeadline: time.Now().Add(10 * time.Second), LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(-10 * time.Second), RefreshDeadline: time.Now().Add(-10 * time.Second),
}}, }},
authenticator.MockAuthenticate{ clients.MockAuthenticate{
RefreshError: errors.New("error"), RefreshError: errors.New("error"),
RefreshResponse: &sessions.SessionState{ RefreshResponse: &sessions.SessionState{
User: "user", User: "user",
@ -456,7 +459,7 @@ func TestProxy_Authenticate(t *testing.T) {
}}, true}, }}, true},
{"bad refresh authenticator", {"bad refresh authenticator",
"https://corp.example.com/", "https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"}, policy,
&sessions.MockSessionStore{ &sessions.MockSessionStore{
Session: &sessions.SessionState{ Session: &sessions.SessionState{
User: "user", User: "user",
@ -468,7 +471,7 @@ func TestProxy_Authenticate(t *testing.T) {
RefreshDeadline: time.Now().Add(-10 * time.Second), RefreshDeadline: time.Now().Add(-10 * time.Second),
}, },
}, },
authenticator.MockAuthenticate{ clients.MockAuthenticate{
RefreshError: errors.New("error"), RefreshError: errors.New("error"),
RefreshResponse: &sessions.SessionState{ RefreshResponse: &sessions.SessionState{
User: "user", User: "user",
@ -483,7 +486,7 @@ func TestProxy_Authenticate(t *testing.T) {
{"good", {"good",
"https://corp.example.com/", "https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"}, policy,
&sessions.MockSessionStore{Session: &sessions.SessionState{ &sessions.MockSessionStore{Session: &sessions.SessionState{
User: "user", User: "user",
Email: "email@email.com", Email: "email@email.com",
@ -497,7 +500,7 @@ func TestProxy_Authenticate(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
opts := testOptions() opts := testOptions()
opts.Routes = tt.mux opts.Policy = tt.mux
p, err := New(opts) p, err := New(opts)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View file

@ -5,7 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
"net" stdlog "log"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
@ -16,34 +16,41 @@ import (
"github.com/pomerium/pomerium/internal/cryptutil" "github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/policy"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/templates" "github.com/pomerium/pomerium/internal/templates"
"github.com/pomerium/pomerium/proxy/authenticator" "github.com/pomerium/pomerium/proxy/clients"
) )
const ( const (
// HeaderJWT is the header key for pomerium proxy's JWT signature. // HeaderJWT is the header key containing JWT signed user details.
HeaderJWT = "x-pomerium-jwt-assertion" HeaderJWT = "x-pomerium-jwt-assertion"
// HeaderUserID represents the header key for the user that is passed to the client. // HeaderUserID is the header key containing the user's id.
HeaderUserID = "x-pomerium-authenticated-user-id" HeaderUserID = "x-pomerium-authenticated-user-id"
// HeaderEmail represents the header key for the email that is passed to the client. // HeaderEmail is the header key containing the user's email.
HeaderEmail = "x-pomerium-authenticated-user-email" HeaderEmail = "x-pomerium-authenticated-user-email"
// HeaderGroups represents the header key for the groups that is passed to the client. // HeaderGroups is the header key containing the user's groups.
HeaderGroups = "x-pomerium-authenticated-user-groups" HeaderGroups = "x-pomerium-authenticated-user-groups"
) )
// Options represents the configurations available for the proxy service. // Options represents the configurations available for the proxy service.
type Options struct { type Options struct {
Policy string `envconfig:"POLICY"`
PolicyFile string `envconfig:"POLICY_FILE"`
// Authenticate service settings // Authenticate service settings
AuthenticateURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"` AuthenticateURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
AuthenticateInternalAddr string `envconfig:"AUTHENTICATE_INTERNAL_URL"` AuthenticateInternalAddr string `envconfig:"AUTHENTICATE_INTERNAL_URL"`
OverrideCertificateName string `envconfig:"OVERRIDE_CERTIFICATE_NAME"` // Authorize service settings
AuthenticatePort int `envconfig:"AUTHENTICATE_SERVICE_PORT"` AuthorizeURL *url.URL `envconfig:"AUTHORIZE_SERVICE_URL"`
CA string `envconfig:"CERTIFICATE_AUTHORITY"` AuthorizeInternalAddr string `envconfig:"AUTHORIZE_INTERNAL_URL"`
CAFile string `envconfig:"CERTIFICATE_AUTHORITY_FILE"` // Settings to enable custom behind-the-ingress service communication
OverrideCertificateName string `envconfig:"OVERRIDE_CERTIFICATE_NAME"`
CA string `envconfig:"CERTIFICATE_AUTHORITY"`
CAFile string `envconfig:"CERTIFICATE_AUTHORITY_FILE"`
// SigningKey is a base64 encoded private key used to add a JWT-signature to proxied requests. // SigningKey is a base64 encoded private key used to add a JWT-signature.
// See : https://www.pomerium.io/guide/signed-headers.html // https://www.pomerium.io/docs/signed-headers.html
SigningKey string `envconfig:"SIGNING_KEY"` SigningKey string `envconfig:"SIGNING_KEY"`
// SharedKey is a 32 byte random key used to authenticate access between services. // SharedKey is a 32 byte random key used to authenticate access between services.
SharedKey string `envconfig:"SHARED_SECRET"` SharedKey string `envconfig:"SHARED_SECRET"`
@ -69,9 +76,7 @@ var defaultOptions = &Options{
CookieSecure: true, CookieSecure: true,
CookieExpire: time.Duration(14) * time.Hour, CookieExpire: time.Duration(14) * time.Hour,
CookieRefresh: time.Duration(30) * time.Minute, CookieRefresh: time.Duration(30) * time.Minute,
DefaultUpstreamTimeout: time.Duration(10) * time.Second, DefaultUpstreamTimeout: time.Duration(30) * time.Second,
// services
AuthenticatePort: 443,
} }
// OptionsFromEnvConfig builds the IdentityProvider service's configuration // OptionsFromEnvConfig builds the IdentityProvider service's configuration
@ -87,15 +92,36 @@ func OptionsFromEnvConfig() (*Options, error) {
// Validate checks that proper configuration settings are set to create // Validate checks that proper configuration settings are set to create
// a proper Proxy instance // a proper Proxy instance
func (o *Options) Validate() error { func (o *Options) Validate() error {
if len(o.Routes) == 0 { if len(o.Routes) != 0 {
return errors.New("missing setting: routes") return errors.New("routes setting is deprecated, use policy instead")
} }
for to, from := range o.Routes { if o.Policy == "" && o.PolicyFile == "" {
if _, err := urlParse(to); err != nil { return errors.New("proxy: either `POLICY` or `POLICY_FILE` must be non-nil")
return fmt.Errorf("could not parse origin %s as url : %q", to, err) }
var policies []policy.Policy
var err error
if o.Policy != "" {
confBytes, err := base64.StdEncoding.DecodeString(o.Policy)
if err != nil {
return fmt.Errorf("proxy: `POLICY` is invalid base64 %v", err)
} }
if _, err := urlParse(from); err != nil { policies, err = policy.FromConfig(confBytes)
return fmt.Errorf("could not parse destination %s as url : %q", to, err) if err != nil {
return fmt.Errorf("proxy: `POLICY` %v", err)
}
}
if o.PolicyFile != "" {
policies, err = policy.FromConfigFile(o.PolicyFile)
if err != nil {
return fmt.Errorf("proxy: `POLICY_FILE` %v", err)
}
}
for _, p := range policies {
if _, err := urlParse(p.To); err != nil {
return fmt.Errorf("could not parse source %s url: %v", p.To, err)
}
if _, err := urlParse(p.From); err != nil {
return fmt.Errorf("could not parse destination %s url: %v", p.From, err)
} }
} }
if o.AuthenticateURL == nil { if o.AuthenticateURL == nil {
@ -104,6 +130,12 @@ func (o *Options) Validate() error {
if o.AuthenticateURL.Scheme != "https" { if o.AuthenticateURL.Scheme != "https" {
return errors.New("authenticate-service-url must be a valid https url") return errors.New("authenticate-service-url must be a valid https url")
} }
if o.AuthorizeURL == nil {
return errors.New("missing setting: authorize-service-url")
}
if o.AuthorizeURL.Scheme != "https" {
return errors.New("authorize-service-url must be a valid https url")
}
if o.CookieSecret == "" { if o.CookieSecret == "" {
return errors.New("missing setting: cookie-secret") return errors.New("missing setting: cookie-secret")
} }
@ -130,9 +162,12 @@ func (o *Options) Validate() error {
type Proxy struct { type Proxy struct {
SharedKey string SharedKey string
// services // authenticate service
AuthenticateURL *url.URL AuthenticateURL *url.URL
AuthenticateClient authenticator.Authenticator AuthenticateClient clients.Authenticator
// authorize service
AuthorizeClient clients.Authorizer
// session // session
cipher cryptutil.Cipher cipher cryptutil.Cipher
@ -146,8 +181,6 @@ type Proxy struct {
// New takes a Proxy service from options and a validation function. // New takes a Proxy service from options and a validation function.
// Function returns an error if options fail to validate. // Function returns an error if options fail to validate.
//
// Caller responsible for closing AuthenticateConn.
func New(opts *Options) (*Proxy, error) { func New(opts *Options) (*Proxy, error) {
if opts == nil { if opts == nil {
return nil, errors.New("options cannot be nil") return nil, errors.New("options cannot be nil")
@ -188,34 +221,48 @@ func New(opts *Options) (*Proxy, error) {
redirectURL: &url.URL{Path: "/.pomerium/callback"}, redirectURL: &url.URL{Path: "/.pomerium/callback"},
templates: templates.New(), templates: templates.New(),
} }
var policies []policy.Policy
for from, to := range opts.Routes { if opts.Policy != "" {
fromURL, _ := urlParse(from) confBytes, _ := base64.StdEncoding.DecodeString(opts.Policy)
toURL, _ := urlParse(to) policies, _ = policy.FromConfig(confBytes)
reverseProxy := NewReverseProxy(toURL) } else {
handler, err := NewReverseProxyHandler(opts, reverseProxy, fromURL.Host, toURL.Host) policies, _ = policy.FromConfigFile(opts.PolicyFile)
}
for _, route := range policies {
proxy := NewReverseProxy(route.Destination)
handler, err := NewReverseProxyHandler(opts, proxy, &route)
if err != nil { if err != nil {
return nil, err return nil, err
} }
p.Handle(fromURL.Host, handler) p.Handle(route.Source.Host, handler)
log.Info().Str("from", fromURL.Host).Str("to", toURL.String()).Msg("proxy: new route") log.Info().Str("src", route.Source.Host).Str("dst", route.Destination.Host).Msg("proxy: new route")
} }
p.AuthenticateClient, err = authenticator.New( p.AuthenticateClient, err = clients.NewAuthenticateClient("grpc",
"grpc", &clients.Options{
&authenticator.Options{
Addr: opts.AuthenticateURL.Host, Addr: opts.AuthenticateURL.Host,
InternalAddr: opts.AuthenticateInternalAddr, InternalAddr: opts.AuthenticateInternalAddr,
OverrideCertificateName: opts.OverrideCertificateName, OverrideCertificateName: opts.OverrideCertificateName,
SharedSecret: opts.SharedKey, SharedSecret: opts.SharedKey,
Port: opts.AuthenticatePort,
CA: opts.CA, CA: opts.CA,
CAFile: opts.CAFile, CAFile: opts.CAFile,
}) })
return p, nil if err != nil {
return nil, err
}
p.AuthorizeClient, err = clients.NewAuthorizeClient("grpc",
&clients.Options{
Addr: opts.AuthorizeURL.Host,
InternalAddr: opts.AuthorizeInternalAddr,
OverrideCertificateName: opts.OverrideCertificateName,
SharedSecret: opts.SharedKey,
CA: opts.CA,
CAFile: opts.CAFile,
})
return p, err
} }
// UpstreamProxy stores information necessary for proxying the request back to the upstream. // UpstreamProxy stores information for proxying the request to the upstream.
type UpstreamProxy struct { type UpstreamProxy struct {
name string name string
cookieName string cookieName string
@ -223,19 +270,6 @@ type UpstreamProxy struct {
signer cryptutil.JWTSigner signer cryptutil.JWTSigner
} }
var defaultUpstreamTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 30 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
// deleteUpstreamCookies deletes the session cookie from the request header string. // deleteUpstreamCookies deletes the session cookie from the request header string.
func deleteUpstreamCookies(req *http.Request, cookieName string) { func deleteUpstreamCookies(req *http.Request, cookieName string) {
headers := []string{} headers := []string{}
@ -271,8 +305,10 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// base path provided in target. NewReverseProxy rewrites the Host header. // base path provided in target. NewReverseProxy rewrites the Host header.
func NewReverseProxy(to *url.URL) *httputil.ReverseProxy { func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
proxy := httputil.NewSingleHostReverseProxy(to) proxy := httputil.NewSingleHostReverseProxy(to)
proxy.Transport = defaultUpstreamTransport sublogger := log.With().Str("proxy", to.Host).Logger()
proxy.ErrorLog = stdlog.New(&log.StdLogWrapper{Logger: &sublogger}, "", 0)
// todo(bdd): default is already http.DefaultTransport)
// proxy.Transport = defaultUpstreamTransport
director := proxy.Director director := proxy.Director
proxy.Director = func(req *http.Request) { proxy.Director = func(req *http.Request) {
// Identifies the originating IP addresses of a client connecting to // Identifies the originating IP addresses of a client connecting to
@ -285,36 +321,36 @@ func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
} }
// 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, from, to string) (http.Handler, error) { func NewReverseProxyHandler(o *Options, proxy *httputil.ReverseProxy, route *policy.Policy) (http.Handler, error) {
up := &UpstreamProxy{ up := &UpstreamProxy{
name: to, name: route.Destination.Host,
handler: reverseProxy, handler: proxy,
cookieName: opts.CookieName, cookieName: o.CookieName,
} }
if len(opts.SigningKey) != 0 { if len(o.SigningKey) != 0 {
decodedSigningKey, err := base64.StdEncoding.DecodeString(opts.SigningKey) decodedSigningKey, err := base64.StdEncoding.DecodeString(o.SigningKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
signer, err := cryptutil.NewES256Signer(decodedSigningKey, from) signer, err := cryptutil.NewES256Signer(decodedSigningKey, route.Source.Host)
if err != nil { if err != nil {
return nil, err return nil, err
} }
up.signer = signer up.signer = signer
} }
timeout := opts.DefaultUpstreamTimeout timeout := o.DefaultUpstreamTimeout
timeoutMsg := fmt.Sprintf("%s failed to respond within the %s timeout period", to, timeout) if route.UpstreamTimeout != 0 {
timeout = route.UpstreamTimeout
}
timeoutMsg := fmt.Sprintf("%s failed to respond within the %s timeout period", route.Destination.Host, timeout)
return http.TimeoutHandler(up, timeout, timeoutMsg), nil return http.TimeoutHandler(up, timeout, timeoutMsg), nil
} }
// urlParse adds a scheme if none-exists, addressesing a quirk in how // urlParse wraps url.Parse to add a scheme if none-exists.
// one may expect url.Parse to function when given scheme-less domain is provided. // https://github.com/golang/go/issues/12585
//
// see: https://github.com/golang/go/issues/12585
// see: https://golang.org/pkg/net/url/#Parse
func urlParse(uri string) (*url.URL, error) { func urlParse(uri string) (*url.URL, error) {
if !strings.Contains(uri, "://") { if !strings.Contains(uri, "://") {
uri = fmt.Sprintf("https://%s", uri) uri = fmt.Sprintf("https://%s", uri)
} }
return url.Parse(uri) return url.ParseRequestURI(uri)
} }

View file

@ -1,6 +1,7 @@
package proxy // import "github.com/pomerium/pomerium/proxy" package proxy // import "github.com/pomerium/pomerium/proxy"
import ( import (
"encoding/base64"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
@ -10,6 +11,8 @@ import (
"reflect" "reflect"
"testing" "testing"
"time" "time"
"github.com/pomerium/pomerium/internal/policy"
) )
var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC) var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
@ -47,36 +50,9 @@ func TestOptionsFromEnvConfig(t *testing.T) {
} }
} }
func Test_urlParse(t *testing.T) {
os.Clearenv()
tests := []struct {
name string
uri string
want *url.URL
wantErr bool
}{
{"good url without schema", "accounts.google.com", &url.URL{Scheme: "https", Host: "accounts.google.com"}, false},
{"good url with schema", "https://accounts.google.com", &url.URL{Scheme: "https", Host: "accounts.google.com"}, false},
{"bad url, malformed", "https://accounts.google.^", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := urlParse(tt.uri)
if (err != nil) != tt.wantErr {
t.Errorf("urlParse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("urlParse() = %v, want %v", got, tt.want)
}
})
}
}
func TestNewReverseProxy(t *testing.T) { func TestNewReverseProxy(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200) w.WriteHeader(http.StatusOK)
hostname, _, _ := net.SplitHostPort(r.Host) hostname, _, _ := net.SplitHostPort(r.Host)
w.Write([]byte(hostname)) w.Write([]byte(hostname))
})) }))
@ -101,7 +77,7 @@ func TestNewReverseProxy(t *testing.T) {
func TestNewReverseProxyHandler(t *testing.T) { func TestNewReverseProxyHandler(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200) w.WriteHeader(http.StatusOK)
hostname, _, _ := net.SplitHostPort(r.Host) hostname, _, _ := net.SplitHostPort(r.Host)
w.Write([]byte(hostname)) w.Write([]byte(hostname))
})) }))
@ -111,10 +87,14 @@ func TestNewReverseProxyHandler(t *testing.T) {
backendHostname, backendPort, _ := net.SplitHostPort(backendURL.Host) backendHostname, backendPort, _ := net.SplitHostPort(backendURL.Host)
backendHost := net.JoinHostPort(backendHostname, backendPort) backendHost := net.JoinHostPort(backendHostname, backendPort)
proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/") proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/")
proxyHandler := NewReverseProxy(proxyURL) proxyHandler := NewReverseProxy(proxyURL)
opts := defaultOptions opts := defaultOptions
handle, err := NewReverseProxyHandler(opts, proxyHandler, "from", "to") opts.SigningKey = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
route, err := policy.FromConfig([]byte(`[{"from":"corp.example.com","to":"example.com","timeout":"1s"}]`))
if err != nil {
t.Fatal(err)
}
handle, err := NewReverseProxyHandler(opts, proxyHandler, &route[0])
if err != nil { if err != nil {
t.Errorf("got %q", err) t.Errorf("got %q", err)
} }
@ -133,10 +113,14 @@ func TestNewReverseProxyHandler(t *testing.T) {
} }
func testOptions() *Options { func testOptions() *Options {
authurl, _ := url.Parse("https://sso-auth.corp.beyondperimeter.com") authenticateService, _ := url.Parse("https://authenticate.corp.beyondperimeter.com")
authorizeService, _ := url.Parse("https://authorize.corp.beyondperimeter.com")
configBlob := `[{"from":"corp.example.com","to":"example.com"}]` //valid yaml
policy := base64.URLEncoding.EncodeToString([]byte(configBlob))
return &Options{ return &Options{
Routes: map[string]string{"corp.example.com": "example.com"}, Policy: policy,
AuthenticateURL: authurl, AuthenticateURL: authenticateService,
AuthorizeURL: authorizeService,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=", SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=", CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieName: "pomerium", CookieName: "pomerium",
@ -151,7 +135,7 @@ func TestOptions_Validate(t *testing.T) {
badToRoute.Routes = map[string]string{"^": "example.com"} badToRoute.Routes = map[string]string{"^": "example.com"}
badAuthURL := testOptions() badAuthURL := testOptions()
badAuthURL.AuthenticateURL = nil badAuthURL.AuthenticateURL = nil
authurl, _ := url.Parse("http://sso-auth.corp.beyondperimeter.com") authurl, _ := url.Parse("http://authenticate.corp.beyondperimeter.com")
httpAuthURL := testOptions() httpAuthURL := testOptions()
httpAuthURL.AuthenticateURL = authurl httpAuthURL.AuthenticateURL = authurl
emptyCookieSecret := testOptions() emptyCookieSecret := testOptions()
@ -193,6 +177,7 @@ func TestOptions_Validate(t *testing.T) {
} }
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
good := testOptions() good := testOptions()
shortCookieLength := testOptions() shortCookieLength := testOptions()
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg==" shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
@ -207,7 +192,7 @@ func TestNew(t *testing.T) {
numMuxes int numMuxes int
wantErr bool wantErr bool
}{ }{
{"good - minimum options", good, nil, true, 1, false}, {"good", good, nil, true, 1, false},
{"empty options", &Options{}, nil, false, 0, true}, {"empty options", &Options{}, nil, false, 0, true},
{"nil options", nil, nil, false, 0, true}, {"nil options", nil, nil, false, 0, true},
{"short secret/validate sanity check", shortCookieLength, nil, false, 0, true}, {"short secret/validate sanity check", shortCookieLength, nil, false, 0, true},
@ -224,7 +209,7 @@ func TestNew(t *testing.T) {
t.Errorf("New() expected valid proxy struct") t.Errorf("New() expected valid proxy struct")
} }
if got != nil && len(got.mux) != tt.numMuxes { if got != nil && len(got.mux) != tt.numMuxes {
t.Errorf("New() = num muxes %v, want %v", got, tt.numMuxes) t.Errorf("New() = num muxes \n%+v, want \n%+v", got, tt.numMuxes)
} }
}) })
} }

View file

@ -30,6 +30,9 @@ kubectl create ns pomerium
# kubectl apply -f docs/docs/examples/kubernetes/issuer.le.stage.yml # kubectl apply -f docs/docs/examples/kubernetes/issuer.le.stage.yml
# kubectl get certificate # kubectl get certificate
# add our policy
kubectl create configmap -n pomerium policy --from-literal=POLICY=$(cat policy.example.yaml | base64)
# create our cryptographically random keys # create our cryptographically random keys
kubectl create secret generic -n pomerium shared-secret --from-literal=shared-secret=$(head -c32 /dev/urandom | base64) kubectl create secret generic -n pomerium shared-secret --from-literal=shared-secret=$(head -c32 /dev/urandom | base64)
kubectl create secret generic -n pomerium cookie-secret --from-literal=cookie-secret=$(head -c32 /dev/urandom | base64) kubectl create secret generic -n pomerium cookie-secret --from-literal=cookie-secret=$(head -c32 /dev/urandom | base64)
@ -47,11 +50,14 @@ kubectl create secret tls -n pomerium pomerium-tls --key privkey.pem --cert cert
# kubectl create secret generic -n pomerium idp-client-secret --from-literal=REPLACE_ME # kubectl create secret generic -n pomerium idp-client-secret --from-literal=REPLACE_ME
# Create the proxy & authenticate deployment # Create the proxy & authenticate deployment
kubectl apply -f docs/docs/examples/kubernetes/authorize.deploy.yml
kubectl apply -f docs/docs/examples/kubernetes/authenticate.deploy.yml kubectl apply -f docs/docs/examples/kubernetes/authenticate.deploy.yml
kubectl apply -f docs/docs/examples/kubernetes/proxy.deploy.yml kubectl apply -f docs/docs/examples/kubernetes/proxy.deploy.yml
# Create the proxy & authenticate services # Create the proxy & authenticate services
kubectl apply -f docs/docs/examples/kubernetes/proxy.service.yml kubectl apply -f docs/docs/examples/kubernetes/proxy.service.yml
kubectl apply -f docs/docs/examples/kubernetes/authenticate.service.yml kubectl apply -f docs/docs/examples/kubernetes/authenticate.service.yml
kubectl apply -f docs/docs/examples/kubernetes/authorize.service.yml
# Create and apply the Ingress; this is GKE specific # Create and apply the Ingress; this is GKE specific
kubectl apply -f docs/docs/examples/kubernetes/ingress.yml kubectl apply -f docs/docs/examples/kubernetes/ingress.yml