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

View file

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

View file

@ -12,7 +12,6 @@ func testOptions() *Options {
redirectURL, _ := url.Parse("https://example.com/oauth2/callback")
return &Options{
ProxyRootDomains: []string{"example.com"},
AllowedDomains: []string{"example.com"},
RedirectURL: redirectURL,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
ClientID: "test-client-id",
@ -35,8 +34,6 @@ func TestOptions_Validate(t *testing.T) {
emptyClientID.ClientID = ""
emptyClientSecret := testOptions()
emptyClientSecret.ClientSecret = ""
allowedDomains := testOptions()
allowedDomains.AllowedDomains = nil
proxyRootDomains := testOptions()
proxyRootDomains.ProxyRootDomains = nil
emptyCookieSecret := testOptions()
@ -63,7 +60,6 @@ func TestOptions_Validate(t *testing.T) {
{"no shared secret", badSharedKey, true},
{"no client id", emptyClientID, true},
{"no client secret", emptyClientSecret, true},
{"empty allowed domains", allowedDomains, true},
{"empty root domains", proxyRootDomains, true},
}
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"
import (
"context"
@ -20,7 +22,7 @@ func (p *Authenticate) Authenticate(ctx context.Context, in *pb.AuthenticateRequ
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
func (p *Authenticate) Validate(ctx context.Context, in *pb.ValidateRequest) (*pb.ValidateReply, error) {
isValid, err := p.provider.Validate(ctx, in.IdToken)

View file

@ -78,9 +78,6 @@ func TestAuthenticate_Refresh(t *testing.T) {
false},
{"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 error", "error", nil, true},
// {"test bad time", "bad time", nil, true},
}
for _, tt := range tests {
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
}
// check if session refresh period is up
if session.RefreshPeriodExpired() {
newSession, err := a.provider.Refresh(r.Context(), session)
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
}
@ -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.
// https://tools.ietf.org/html/rfc6749#section-4.2.1
func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
authRedirectURL, err := url.Parse(r.URL.Query().Get("redirect_uri"))
if err != nil {
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
return
}
authRedirectURL = a.RedirectURL.ResolveReference(r.URL)
authRedirectURL := a.RedirectURL.ResolveReference(r.URL)
nonce := fmt.Sprintf("%x", cryptutil.GenerateKey())
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"}
}
// 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)
if err != nil {
log.Error().Err(err).Msg("internal error")

View file

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