authenticate/proxy: add user impersonation, refresh, dashboard (#123)

proxy: Add user dashboard. [GH-123]
proxy/authenticate: Add manual refresh of their session. [GH-73]
authorize: Add administrator (super user) account support. [GH-110]
internal/policy: Allow administrators to impersonate other users. [GH-110]
This commit is contained in:
Bobby DeSimone 2019-05-26 12:33:00 -07:00 committed by GitHub
parent dc2eb9668c
commit 66b4c2d3cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1644 additions and 1006 deletions

View file

@ -1,5 +1,36 @@
# Pomerium Changelog
## vUNRELEASED
### NEW
- Add user dashboard containing information about the current user's session. [GH-123]
- Add functionality allowing users to initiate manual refresh of their session. This is helpful when a user's access control details are updated but their session hasn't updated yet. To prevent abuse, manual refresh is gated by a cooldown (`REFRESH_COOLDOWN`) which defaults to five minutes. [GH-73]
- Add Administrator (super user) account support (`ADMINISTRATORS`). [GH-110]
- Add feature that allows Administrators to impersonate / sign-in as another user from the user dashboard. [GH-110]
- Add docker images and builds for ARM. [GH-95]
- Add support for public, unauthenticated routes. [GH-129]
### CHANGED
- User state is now maintained and scoped at the domain level vs at the route level. [GH-128]
- Error pages contain a link to sign out from the current user session. [GH-100]
- Removed `LifetimeDeadline` from `sessions.SessionState`.
- Removed favicon specific request handling. [GH-131]
- Headers are now configurable via the `HEADERS` configuration variable. [GH-108]
- Refactored proxy and authenticate services to share the same session state cookie. [GH-131]
- Removed instances of extraneous session state saves. [GH-131]
- Changed default behavior when no session is found. Users are now redirected to login instead of being shown an error page.[GH-131]
- Updated routes such that all http handlers are now wrapped with a standard set of middleware. Headers, request id, loggers, and health checks middleware are now applied to all routes including 4xx and 5xx responses. [GH-116]
- Changed docker images to be built from [distroless](https://github.com/GoogleContainerTools/distroless). This fixed an issue with `nsswitch` [GH-97], includes `ca-certificates` and limits the attack surface area of our images. [GH-101]
- Changed HTTP to HTTPS redirect server to be user configurable via `HTTP_REDIRECT_ADDR`. [GH-103]
- `Content-Security-Policy` hash updated to match new UI assets.
### FIXED
- Fixed an issue where policy and routes were being pre-processed incorrectly. [GH-132]
- Fixed an issue where `golint` was not being found in our docker image. [GH-121]
## v0.0.4
### CHANGED

View file

@ -62,21 +62,18 @@ func TestAuthenticate_Refresh(t *testing.T) {
&identity.MockProvider{
RefreshResponse: &sessions.SessionState{
AccessToken: "updated",
LifetimeDeadline: fixedDate,
RefreshDeadline: fixedDate,
}},
&pb.Session{
AccessToken: "original",
LifetimeDeadline: fixedProtoTime,
RefreshDeadline: fixedProtoTime,
},
&pb.Session{
AccessToken: "updated",
LifetimeDeadline: fixedProtoTime,
RefreshDeadline: fixedProtoTime,
},
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}, nil, true},
{"test catch nil", nil, nil, nil, true},
}
for _, tt := range tests {
@ -105,7 +102,6 @@ func TestAuthenticate_Authenticate(t *testing.T) {
if err != nil {
t.Fatalf("expected to be able to create cipher: %v", err)
}
lt := time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC()
rt := time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC()
vtProto, err := ptypes.TimestampProto(rt)
if err != nil {
@ -115,9 +111,7 @@ func TestAuthenticate_Authenticate(t *testing.T) {
want := &sessions.SessionState{
AccessToken: "token1234",
RefreshToken: "refresh4321",
LifetimeDeadline: lt,
RefreshDeadline: rt,
Email: "user@domain.com",
User: "user",
}
@ -125,7 +119,6 @@ func TestAuthenticate_Authenticate(t *testing.T) {
goodReply := &pb.Session{
AccessToken: "token1234",
RefreshToken: "refresh4321",
LifetimeDeadline: vtProto,
RefreshDeadline: vtProto,
Email: "user@domain.com",
User: "user"}

View file

@ -12,12 +12,15 @@ import (
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version"
)
// CSPHeaders adds content security headers for authenticate's handlers
var CSPHeaders = map[string]string{
"Content-Security-Policy": "default-src 'none'; style-src 'self' 'sha256-pSTVzZsFAqd2U3QYu+BoBDtuJWaPM/+qMy/dBRrhb5Y='; img-src 'self';",
"Content-Security-Policy": "default-src 'none'; style-src 'self'" +
" 'sha256-z9MsgkMbQjRSLxzAfN55jB3a9pP0PQ4OHFH8b4iDP6s=' " +
" 'sha256-qnVkQSG7pWu17hBhIw0kCpfEB3XGvt0mNRa6+uM6OUU=' " +
" 'sha256-qOdRsNZhtR+htazbcy7guQl3Cn1cqOw1FcE4d3llae0='; " +
"img-src 'self';",
"Referrer-Policy": "Same-origin",
}
@ -35,7 +38,7 @@ func (a *Authenticate) Handler() http.Handler {
mux.Handle("/oauth2/callback", c.ThenFunc(a.OAuthCallback))
// authenticate-server endpoints
mux.Handle("/sign_in", validate.ThenFunc(a.SignIn))
mux.Handle("/sign_out", validate.ThenFunc(a.SignOut)) // GET POST
mux.Handle("/sign_out", validate.ThenFunc(a.SignOut)) // POST
return mux
}
@ -53,7 +56,7 @@ func (a *Authenticate) authenticate(w http.ResponseWriter, r *http.Request, sess
}
err = a.sessionStore.SaveSession(w, r, session)
if err != nil {
return fmt.Errorf("authenticate: refresh failed : %v", err)
return fmt.Errorf("authenticate: failed saving refreshed session : %v", err)
}
} else {
valid, err := a.provider.Validate(r.Context(), session.IDToken)
@ -86,14 +89,7 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) {
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return
}
a.ProxyCallback(w, r, session)
}
// ProxyCallback redirects the user back to proxy service along with an encrypted payload, as
// url params, of the user's session state as specified in RFC6749 3.1.2.
// https://tools.ietf.org/html/rfc6749#section-3.1.2
func (a *Authenticate) ProxyCallback(w http.ResponseWriter, r *http.Request, session *sessions.SessionState) {
err := r.ParseForm()
err = r.ParseForm()
if err != nil {
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return
@ -104,14 +100,8 @@ func (a *Authenticate) ProxyCallback(w http.ResponseWriter, r *http.Request, ses
httputil.ErrorResponse(w, r, "no state parameter supplied", http.StatusForbidden)
return
}
// redirect url of proxy-service
redirectURI := r.Form.Get("redirect_uri")
if redirectURI == "" {
httputil.ErrorResponse(w, r, "no redirect_uri parameter supplied", http.StatusForbidden)
return
}
redirectURL, err := url.Parse(redirectURI)
redirectURL, err := url.Parse(r.Form.Get("redirect_uri"))
if err != nil {
httputil.ErrorResponse(w, r, "malformed redirect_uri parameter passed", http.StatusBadRequest)
return
@ -145,40 +135,11 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return
}
// pretty safe to say that no matter what heppanes here, we want to revoke the local session
redirectURI := r.Form.Get("redirect_uri")
session, err := a.sessionStore.LoadSession(r)
if err != nil {
log.Error().Err(err).Msg("authenticate: signout failed to load session")
httputil.ErrorResponse(w, r, "No session found to log out", http.StatusBadRequest)
return
}
if r.Method == http.MethodGet {
signature := r.Form.Get("sig")
timestamp := r.Form.Get("ts")
destinationURL, err := url.Parse(redirectURI)
if err != nil {
log.Error().Err(err).Msg("authenticate: malformed destination url")
httputil.ErrorResponse(w, r, "Malformed destination URL", http.StatusBadRequest)
return
}
t := struct {
Redirect string
Signature string
Timestamp string
Destination string
Email string
Version string
}{
Redirect: redirectURI,
Signature: signature,
Timestamp: timestamp,
Destination: destinationURL.Host,
Email: session.Email,
Version: version.FullVersion(),
}
a.templates.ExecuteTemplate(w, "sign_out.html", t)
w.WriteHeader(http.StatusOK)
log.Error().Err(err).Msg("authenticate: no session to signout, redirect and clear")
http.Redirect(w, r, redirectURI, http.StatusFound)
return
}
a.sessionStore.ClearSession(w, r)
@ -196,6 +157,7 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
authRedirectURL := a.RedirectURL.ResolveReference(r.URL)
// generate a nonce to check following authentication with the IdP
nonce := fmt.Sprintf("%x", cryptutil.GenerateKey())
a.csrfStore.SetCSRF(w, r, nonce)
@ -204,6 +166,7 @@ func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
httputil.ErrorResponse(w, r, "Invalid redirect parameter: redirect uri not from the root domain", http.StatusBadRequest)
return
}
// verify proxy url is from the root domain
proxyRedirectURL, err := url.Parse(authRedirectURL.Query().Get("redirect_uri"))
if err != nil || !middleware.SameSubdomain(proxyRedirectURL, a.RedirectURL) {
@ -221,6 +184,7 @@ func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
// concat base64'd nonce and authenticate url to make state
state := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", nonce, authRedirectURL.String())))
// build the provider sign in url
signInURL := a.provider.GetSignInURL(state)
http.Redirect(w, r, signInURL, http.StatusFound)
@ -242,12 +206,14 @@ func (a *Authenticate) OAuthCallback(w http.ResponseWriter, r *http.Request) {
httputil.ErrorResponse(w, r, "Internal Error", http.StatusInternalServerError)
return
}
// redirect back to the proxy-service
// redirect back to the proxy-service via sign_in
log.Info().Interface("redirect", redirect).Msg("proxy: OAuthCallback")
http.Redirect(w, r, redirect, http.StatusFound)
}
// getOAuthCallback completes the oauth cycle from an identity provider's callback
func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) (string, error) {
// handle the callback response from the identity provider
err := r.ParseForm()
if err != nil {
return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: err.Error()}
@ -263,12 +229,14 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
return "", httputil.HTTPError{Code: http.StatusBadRequest, Message: "Missing Code"}
}
// validate the returned code with the identity provider
session, err := a.provider.Authenticate(code)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("authenticate: error redeeming authenticate code")
return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: err.Error()}
}
// okay, time to go back to the proxy service.
bytes, err := base64.URLEncoding.DecodeString(r.Form.Get("state"))
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed decoding state")
@ -281,30 +249,26 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
nonce := s[0]
redirect := s[1]
c, err := a.csrfStore.GetCSRF(r)
if err != nil {
log.FromRequest(r).Error().Err(err).Interface("s", s).Msg("authenticate: bad csrf")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Missing CSRF token"}
}
a.csrfStore.ClearCSRF(w, r)
if c.Value != nonce {
log.FromRequest(r).Error().Err(err).Msg("authenticate: csrf mismatch")
defer a.csrfStore.ClearCSRF(w, r)
if err != nil || c.Value != nonce {
log.FromRequest(r).Error().Err(err).Msg("authenticate: csrf failure")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "CSRF failed"}
}
redirectURL, err := url.Parse(redirect)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("authenticate: couldn't parse redirect url")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Couldn't parse redirect url"}
log.FromRequest(r).Error().Err(err).Msg("authenticate: malformed redirect url")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Malformed redirect url"}
}
// sanity check, we are redirecting back to the same subdomain right?
if !middleware.SameSubdomain(redirectURL, a.RedirectURL) {
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Invalid Redirect URI domain"}
}
err = a.sessionStore.SaveSession(w, r, session)
if err != nil {
log.Error().Err(err).Msg("internal error")
log.Error().Err(err).Msg("authenticate: failed saving new session")
return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: "Internal Error"}
}
return redirect, nil
}

View file

@ -183,96 +183,6 @@ func (a mockCipher) Unmarshal(s string, i interface{}) error {
}
return nil
}
func TestAuthenticate_ProxyCallback(t *testing.T) {
tests := []struct {
name string
uri string
state string
authCode string
sessionState *sessions.SessionState
sessionStore sessions.SessionStore
wantCode int
wantBody string
}{
{"good", "https://corp.pomerium.io/", "state", "code",
&sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * time.Second),
},
&sessions.MockSessionStore{},
302,
"<a href=\"https://corp.pomerium.io/?code=ok&amp;state=state\">Found</a>."},
{"no state",
"https://corp.pomerium.io/",
"",
"code",
&sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * time.Second),
},
&sessions.MockSessionStore{},
403,
"no state parameter supplied"},
{"no redirect_url",
"",
"state",
"code",
&sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * time.Second),
},
&sessions.MockSessionStore{},
403,
"no redirect_uri parameter"},
{"malformed redirect_url",
"https://pomerium.com%zzzzz",
"state",
"code",
&sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * time.Second),
},
&sessions.MockSessionStore{},
400,
"malformed redirect_uri"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &Authenticate{
sessionStore: tt.sessionStore,
cipher: mockCipher{},
}
u, _ := url.Parse("https://pomerium.io/redirect")
params, _ := url.ParseQuery(u.RawQuery)
params.Set("code", tt.authCode)
params.Set("state", tt.state)
params.Set("redirect_uri", tt.uri)
u.RawQuery = params.Encode()
r := httptest.NewRequest("GET", u.String(), nil)
w := httptest.NewRecorder()
a.ProxyCallback(w, r, tt.sessionState)
if status := w.Code; status != tt.wantCode {
t.Errorf("handler returned wrong status code: got %v want %v", status, tt.wantCode)
}
if body := w.Body.String(); !strings.Contains(body, tt.wantBody) {
t.Errorf("handler returned wrong body Body: got \n%s \n%s", body, tt.wantBody)
}
})
}
}
func Test_getAuthCodeRedirectURL(t *testing.T) {
tests := []struct {
@ -350,59 +260,6 @@ func TestAuthenticate_SignOut(t *testing.T) {
},
http.StatusBadRequest,
"could not revoke"},
{"good get",
http.MethodGet,
"https://corp.pomerium.io/",
"sig",
"ts",
identity.MockProvider{},
&sessions.MockSessionStore{
Session: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
Email: "blah@blah.com",
RefreshDeadline: time.Now().Add(10 * time.Second),
},
},
http.StatusOK,
"This will also sign you out of other internal apps."},
{"cannot load session",
http.MethodGet,
"https://corp.pomerium.io/",
"sig",
"ts",
identity.MockProvider{},
&sessions.MockSessionStore{
LoadError: errors.New("uh oh"),
Session: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
Email: "blah@blah.com",
RefreshDeadline: time.Now().Add(10 * time.Second),
},
},
http.StatusBadRequest,
"No session found to log out"},
{"bad redirect url get",
http.MethodGet,
"https://pomerium.com%zzzzz",
"sig",
"ts",
identity.MockProvider{},
&sessions.MockSessionStore{
Session: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
Email: "blah@blah.com",
RefreshDeadline: time.Now().Add(10 * time.Second),
},
},
http.StatusBadRequest,
"Error"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View file

@ -49,17 +49,17 @@ func New(opts *config.Options) (*Authorize, error) {
return &Authorize{
SharedKey: string(sharedKey),
identityAccess: NewIdentityWhitelist(opts.Policies),
identityAccess: NewIdentityWhitelist(opts.Policies, opts.Administrators),
}, nil
}
// NewIdentityWhitelist returns an indentity validator.
// todo(bdd) : a radix-tree implementation is probably more efficient
func NewIdentityWhitelist(policies []policy.Policy, admins []string) IdentityValidator {
return newIdentityWhitelistMap(policies, admins)
}
// 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

@ -9,7 +9,24 @@ import (
// 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})
func (a *Authorize) Authorize(ctx context.Context, in *pb.Identity) (*pb.AuthorizeReply, error) {
ok := a.ValidIdentity(in.Route,
&Identity{
User: in.User,
Email: in.Email,
Groups: in.Groups,
ImpersonateEmail: in.ImpersonateEmail,
ImpersonateGroups: in.ImpersonateGroups,
})
return &pb.AuthorizeReply{IsValid: ok}, nil
}
// IsAdmin validates the user is an administrative user.
func (a *Authorize) IsAdmin(ctx context.Context, in *pb.Identity) (*pb.IsAdminReply, error) {
ok := a.identityAccess.IsAdmin(
&Identity{
Email: in.Email,
Groups: in.Groups,
})
return &pb.IsAdminReply{IsAdmin: ok}, nil
}

View file

@ -16,12 +16,12 @@ func TestAuthorize_Authorize(t *testing.T) {
name string
SharedKey string
identityAccess IdentityValidator
in *pb.AuthorizeRequest
in *pb.Identity
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},
{"valid authorization request", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", &MockIdentityValidator{ValidResponse: true}, &pb.Identity{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.AuthorizeReply{IsValid: true}, false},
{"invalid authorization request", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", &MockIdentityValidator{ValidResponse: false}, &pb.Identity{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) {
@ -37,3 +37,33 @@ func TestAuthorize_Authorize(t *testing.T) {
})
}
}
func TestAuthorize_IsAdmin(t *testing.T) {
t.Parallel()
tests := []struct {
name string
identityAccess IdentityValidator
in *pb.Identity
want *pb.IsAdminReply
wantErr bool
}{
{"valid authorization request", &MockIdentityValidator{IsAdminResponse: true}, &pb.Identity{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.IsAdminReply{IsAdmin: true}, false},
{"invalid authorization request", &MockIdentityValidator{IsAdminResponse: false}, &pb.Identity{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.IsAdminReply{IsAdmin: false}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &Authorize{
SharedKey: "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y",
identityAccess: tt.identityAccess,
}
got, err := a.IsAdmin(context.Background(), tt.in)
if (err != nil) != tt.wantErr {
t.Errorf("Authorize.IsAdmin() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Authorize.IsAdmin() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -14,14 +14,26 @@ type Identity struct {
User string
Email string
Groups []string
// Impersonation
ImpersonateEmail string
ImpersonateGroups []string
}
// EmailDomain returns the domain of the identity's email.
func (i *Identity) EmailDomain() string {
if i.Email == "" {
// IsImpersonating returns whether the user is trying to impersonate another
// user email or group.
func (i *Identity) IsImpersonating() bool {
if i.ImpersonateEmail != "" || len(i.ImpersonateGroups) != 0 {
return true
}
return false
}
// EmailDomain returns the domain portion of an email.
func EmailDomain(email string) string {
if email == "" {
return ""
}
comp := strings.Split(i.Email, "@")
comp := strings.Split(email, "@")
if len(comp) != 2 || comp[0] == "" {
return ""
}
@ -32,96 +44,141 @@ func (i *Identity) EmailDomain() string {
// to a given route.
type IdentityValidator interface {
Valid(string, *Identity) bool
IsAdmin(*Identity) bool
}
type identityWhitelist struct {
type whitelist struct {
sync.RWMutex
m map[string]bool
access map[string]bool
admins 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)
func newIdentityWhitelistMap(policies []policy.Policy, admins []string) *whitelist {
var wl whitelist
wl.access = make(map[string]bool, len(policies)*3)
for _, p := range policies {
for _, group := range p.AllowedGroups {
wl.PutGroup(p.From, group)
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")
wl.PutDomain(p.From, domain)
log.Debug().Str("route", p.From).Str("domain", 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")
wl.PutEmail(p.From, email)
log.Debug().Str("route", p.From).Str("email", email).Msg("add email")
}
}
return &im
wl.admins = make(map[string]bool, len(admins))
for _, admin := range admins {
wl.PutAdmin(admin)
log.Debug().Str("admin", admin).Msg("add administrator")
}
return &wl
}
// 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 {
func (wl *whitelist) Valid(route string, i *Identity) bool {
email := i.Email
domain := EmailDomain(email)
groups := i.Groups
// if user is admin, and wants to impersonate, override values
if wl.IsAdmin(i) && i.IsImpersonating() {
email = i.ImpersonateEmail
domain = EmailDomain(email)
groups = i.ImpersonateGroups
}
if ok := wl.Email(route, email); ok {
return ok
}
if ok := m.Email(route, i.Email); ok {
if ok := wl.Domain(route, domain); ok {
return ok
}
for _, group := range i.Groups {
if ok := m.Group(route, group); ok {
for _, group := range groups {
if ok := wl.Group(route, group); ok {
return ok
}
}
return false
}
func (wl *whitelist) IsAdmin(i *Identity) bool {
if ok := wl.Admin(i.Email); 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)]
func (wl *whitelist) Group(route, group string) bool {
wl.RLock()
defer wl.RUnlock()
return wl.access[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()
func (wl *whitelist) PutGroup(route, group string) {
wl.Lock()
wl.access[fmt.Sprintf("%s|group:%s", route, group)] = true
wl.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)]
func (wl *whitelist) Domain(route, domain string) bool {
wl.RLock()
defer wl.RUnlock()
return wl.access[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()
func (wl *whitelist) PutDomain(route, domain string) {
wl.Lock()
wl.access[fmt.Sprintf("%s|domain:%s", route, domain)] = true
wl.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)]
func (wl *whitelist) Email(route, email string) bool {
wl.RLock()
defer wl.RUnlock()
return wl.access[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()
func (wl *whitelist) PutEmail(route, email string) {
wl.Lock()
wl.access[fmt.Sprintf("%s|email:%s", route, email)] = true
wl.Unlock()
}
// PutEmail adds an admin entry
func (wl *whitelist) PutAdmin(admin string) {
wl.Lock()
wl.admins[admin] = true
wl.Unlock()
}
// Admin checks if the email matches an admin
func (wl *whitelist) Admin(admin string) bool {
wl.RLock()
defer wl.RUnlock()
return wl.admins[admin]
}
// MockIdentityValidator is a mock implementation of IdentityValidator
type MockIdentityValidator struct{ ValidResponse bool }
type MockIdentityValidator struct {
ValidResponse bool
IsAdminResponse bool
}
// Valid is a mock implementation IdentityValidator's Valid method
func (mv *MockIdentityValidator) Valid(u string, i *Identity) bool { return mv.ValidResponse }
// IsAdmin is a mock implementation IdentityValidator's IsAdmin method
func (mv *MockIdentityValidator) IsAdmin(i *Identity) bool { return mv.IsAdminResponse }

View file

@ -21,8 +21,7 @@ func TestIdentity_EmailDomain(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
i := &Identity{Email: tt.Email}
if got := i.EmailDomain(); got != tt.want {
if got := EmailDomain(tt.Email); got != tt.want {
t.Errorf("Identity.EmailDomain() = %v, want %v", got, tt.want)
}
})
@ -36,25 +35,39 @@ func Test_IdentityWhitelistMap(t *testing.T) {
policies []policy.Policy
route string
Identity *Identity
admins []string
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},
{"valid domain", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, nil, true},
{"valid domain with admins", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, []string{"admin@example.com"}, true},
{"invalid domain prepend", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "a@1example.com"}, nil, false},
{"invalid domain postpend", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com2"}, nil, false},
{"valid group", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"admin"}}, nil, true},
{"invalid group", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyone"}}, nil, false},
{"invalid empty", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{""}}, nil, false},
{"valid group multiple", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyone", "admin"}}, nil, true},
{"invalid group multiple", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyones", "sadmin"}}, nil, false},
{"valid user email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, nil, true},
{"invalid user email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "user2@example.com"}, nil, false},
{"empty everything", []policy.Policy{{From: "example.com"}}, "example.com", &Identity{Email: "user2@example.com"}, nil, false},
// impersonation related
{"admin not impersonating allowed", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "admin@example.com"}, []string{"admin@example.com"}, true},
{"admin not impersonating denied", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com"}, []string{"admin@admin-domain.com"}, false},
{"impersonating match domain", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, true},
{"impersonating does not match domain", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, false},
{"impersonating match email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, true},
{"impersonating does not match email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, false},
{"impersonating match groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"support"}}, []string{"admin@admin-domain.com"}, true},
{"impersonating match many groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"a", "b", "c", "support"}}, []string{"admin@admin-domain.com"}, true},
{"impersonating does not match groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support"}}, []string{"admin@admin-domain.com"}, false},
{"impersonating does not match many groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support", "b", "c"}}, []string{"admin@admin-domain.com"}, false},
{"impersonating does not match empty groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{""}}, []string{"admin@admin-domain.com"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wl := NewIdentityWhitelist(tt.policies)
wl := NewIdentityWhitelist(tt.policies, tt.admins)
if got := wl.Valid(tt.route, tt.Identity); got != tt.want {
t.Errorf("IdentityACLMap.Allowed() = %v, want %v", got, tt.want)
t.Errorf("wl.Valid() = %v, want %v", got, tt.want)
}
})

View file

@ -146,16 +146,14 @@ func wrapMiddleware(o *config.Options, mux *http.ServeMux) http.Handler {
c = c.Append(middleware.NewHandler(log.Logger))
c = c.Append(middleware.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
middleware.FromRequest(r).Debug().
Str("service", o.Services).
Str("method", r.Method).
Str("url", r.URL.String()).
Int("status", status).
Int("size", size).
Dur("duration", duration).
Str("user", r.Header.Get(proxy.HeaderUserID)).
Int("size", size).
Int("status", status).
Str("email", r.Header.Get(proxy.HeaderEmail)).
Str("group", r.Header.Get(proxy.HeaderGroups)).
// Str("sig", r.Header.Get(proxy.HeaderJWT)).
Str("method", r.Method).
Str("service", o.Services).
Str("url", r.URL.String()).
Msg("http-request")
}))
if o != nil && len(o.Headers) != 0 {

View file

@ -6,8 +6,8 @@
# export SERVICE="all" # optional, default is all
# export LOG_LEVEL="info" # optional, default is debug
export AUTHENTICATE_SERVICE_URL=https://authenticate.corp.example.com
export AUTHORIZE_SERVICE_URL=https://authorize.corp.example.com
export AUTHENTICATE_SERVICE_URL=https://authenticate.corp.beyondperimeter.com
export AUTHORIZE_SERVICE_URL=https://authorize.corp.beyondperimeter.com
# Certificates can be loaded as files or base64 encoded bytes. If neither is set, a
# pomerium will attempt to locate a pair in the root directory

View file

@ -3,7 +3,7 @@
# Proxied routes and per-route policies are defined in a policy block
policy: # Omit the 'policy' key if you are encoding it into an environment variable
- from: httpbin.corp.beyondperimeter.com
to: http://httpbin
to: http://localhost:8000
allowed_domains:
- pomerium.io
cors_allow_preflight: true
@ -20,6 +20,6 @@ policy: # Omit the 'policy' key if you are encoding it into an environment varia
- admins
- developers
- from: hello.corp.beyondperimeter.com
to: http://hello:8080
to: http://localhost:8080
allowed_groups:
- admins
- admins@pomerium.io

View file

@ -2,7 +2,7 @@
address: ":8443" # optional, default is 443
pomerium_debug: true # optional, default is false
service: "all" # optional, default is all
# log_level: "info" # optional, default is debug
log_level: "info" # optional, default is debug
authenticate_service_url: https://authenticate.corp.pomerium.io:8443
authorize_service_url: https://authorize.corp.pomerium.io:8443
@ -12,10 +12,12 @@ authorize_service_url: https://authorize.corp.pomerium.io:8443
certificate_file: "./cert.pem" # optional, defaults to `./cert.pem`
certificate_key_file: "./privkey.pem" # optional, defaults to `./certprivkey.pem`
certificate_authority_file: "./cert.pem"
# base64 encoded cert, eg. `base64 -i cert.pem` / `base64 -i privkey.pem`
# certificate: |
# "xxxxxx" # base64 encoded cert, eg. `base64 -i cert.pem`
# "xxxxxx"
# certificate_key: |
# "xxxx" # base64 encoded key, eg. `base64 -i privkey.pem`
# "xxxx"
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
shared_secret: hsJIQsx9KKx4qVlggg/T3AuLTmVu0uHhwTQgMPlVs7U=

View file

@ -40,6 +40,15 @@ Service mode sets the pomerium service(s) to run. If testing, you may want to se
Address specifies the host and port to serve HTTPS and gRPC requests from. If empty, `:https`/`:443` is used.
### Administrators
- Environmental Variable: `ADMINISTRATORS`
- Config File Key: `administrators`
- Type: slice of `string`
- Example: `"admin@example.com,admin2@example.com"`
Administrative users are [super user](https://en.wikipedia.org/wiki/Superuser) that can sign in as another user or group. User impersonation allows administrators to temporarily sign in as a different user.
### Shared Secret
- Environmental Variable: `SHARED_SECRET`
@ -206,8 +215,7 @@ Allow unauthenticated HTTP OPTIONS requests as [per the CORS spec](https://devel
- Optional
- Default: `false`
**Use with caution:** Allow all requests for a given route, bypassing authentication and authorization.
Suitable for publicly exposed web services.
**Use with caution:** Allow all requests for a given route, bypassing authentication and authorization. Suitable for publicly exposed web services.
If this setting is enabled, no whitelists (e.g. Allowed Users) should be provided in this route.
@ -374,6 +382,16 @@ By default, conservative [secure HTTP headers](https://www.owasp.org/index.php/O
![pomerium security headers](./security-headers.png)
### Refresh Cooldown
- Environmental Variable: `REFRESH_COOLDOWN`
- Config File Key: `refresh_cooldown`
- Type: [Duration](https://golang.org/pkg/time/#Duration) `string`
- Example: `10m`, `1h45m`
- Default: `5m`
Refresh cooldown is the minimum amount of time between allowed manually refreshed sessions.
[base64 encoded]: https://en.wikipedia.org/wiki/Base64
[environmental variables]: https://en.wikipedia.org/wiki/Environment_variable
[identity provider]: ./identity-providers.md

9
go.mod
View file

@ -5,15 +5,16 @@ go 1.12
require (
github.com/golang/mock v1.2.0
github.com/golang/protobuf v1.3.1
github.com/pomerium/envconfig v1.5.0
github.com/magiconair/properties v1.8.1 // indirect
github.com/pomerium/go-oidc v2.0.0+incompatible
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/rs/zerolog v1.12.0
github.com/rs/zerolog v1.14.3
github.com/spf13/viper v1.3.2
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f
golang.org/x/net v0.0.0-20190522155817-f3200d17e092
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
golang.org/x/sys v0.0.0-20190524152521-dbbf3f1254d4 // indirect
golang.org/x/text v0.3.2 // indirect
google.golang.org/api v0.1.0
google.golang.org/grpc v1.19.1

94
go.sum
View file

@ -4,38 +4,22 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
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/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
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.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
@ -43,61 +27,35 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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.5.0 h1:OeYS/p6AUxKFqCZHM5BG7pUb0m3MkaC1ZhRLPTHbk8g=
github.com/pomerium/envconfig v1.5.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/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/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rs/zerolog v1.12.0 h1:aqZ1XRadoS8IBknR5IDFvGzbHly1X9ApIqOroooQF/c=
github.com/rs/zerolog v1.12.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0=
github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
@ -108,41 +66,28 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7 h1:Qe/u+eY379X4He4GBMFZYu3pmh1ML5yT1aL1ndNM1zQ=
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -152,24 +97,23 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
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-20190423024810-112230192c58/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-20180905080454-ebe1bf3edb33/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-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190524152521-dbbf3f1254d4 h1:VSJ45BzqrVgR4clSx415y1rHH7QAGhGt71J0ZmhLYrc=
golang.org/x/sys v0.0.0-20190524152521-dbbf3f1254d4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
@ -184,18 +128,12 @@ google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM=
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
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 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
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=

View file

@ -94,6 +94,10 @@ type Options struct {
Policies []policy.Policy
// Administrators contains a set of emails with users who have super user
// (sudo) access including the ability to impersonate other users' access
Administrators []string `mapstructure:"administrators"`
// AuthenticateInternalAddr is used as an override when using a load balancer
// or ingress that does not natively support routing gRPC.
AuthenticateInternalAddr string `mapstructure:"authenticate_internal_url"`
@ -116,12 +120,15 @@ type Options struct {
// Headers to set on all proxied requests. Add a 'disable' key map to turn off.
Headers map[string]string `mapstructure:"headers"`
// RefreshCooldown limits the rate a user can refresh her session
RefreshCooldown time.Duration `mapstructure:"refresh_cooldown"`
// Sub-routes
Routes map[string]string `mapstructure:"routes"`
DefaultUpstreamTimeout time.Duration `mapstructure:"default_upstream_timeout"`
}
// NewOptions returns a new options struct with default vaules
// NewOptions returns a new options struct with default values
func NewOptions() *Options {
o := &Options{
Debug: false,
@ -148,6 +155,7 @@ func NewOptions() *Options {
IdleTimeout: 5 * time.Minute,
AuthenticateURL: new(url.URL),
AuthorizeURL: new(url.URL),
RefreshCooldown: time.Duration(5 * time.Minute),
}
return o
}

View file

@ -0,0 +1,28 @@
package cryptutil // import "github.com/pomerium/pomerium/internal/cryptutil"
// MockCipher MockCSRFStore is a mock implementation of Cipher.
type MockCipher struct {
EncryptResponse []byte
EncryptError error
DecryptResponse []byte
DecryptError error
MarshalResponse string
MarshalError error
UnmarshalError error
}
// Encrypt is a mock implementation of MockCipher.
func (mc MockCipher) Encrypt(b []byte) ([]byte, error) { return mc.EncryptResponse, mc.EncryptError }
// Decrypt is a mock implementation of MockCipher.
func (mc MockCipher) Decrypt(b []byte) ([]byte, error) { return mc.DecryptResponse, mc.DecryptError }
// Marshal is a mock implementation of MockCipher.
func (mc MockCipher) Marshal(i interface{}) (string, error) {
return mc.MarshalResponse, mc.MarshalError
}
// Unmarshal is a mock implementation of MockCipher.
func (mc MockCipher) Unmarshal(s string, i interface{}) error {
return mc.UnmarshalError
}

View file

@ -30,7 +30,11 @@ func CodeForError(err error) int {
}
// ErrorResponse renders an error page for errors given a message and a status code.
// If no message is passed, defaults to the text of the status code.
func ErrorResponse(rw http.ResponseWriter, req *http.Request, message string, code int) {
if message == "" {
message = http.StatusText(code)
}
if req.Header.Get("Accept") == "application/json" {
var response struct {
Error string `json:"error"`

View file

@ -170,7 +170,6 @@ func (p *GoogleProvider) Authenticate(code string) (*sessions.SessionState, erro
AccessToken: oauth2Token.AccessToken,
RefreshToken: oauth2Token.RefreshToken,
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
Email: claims.Email,
User: idToken.Subject,
Groups: groups,

View file

@ -113,7 +113,6 @@ func (p *AzureProvider) Authenticate(code string) (*sessions.SessionState, error
AccessToken: oauth2Token.AccessToken,
RefreshToken: oauth2Token.RefreshToken,
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
Email: claims.Email,
User: idToken.Subject,
Groups: groups,

View file

@ -87,7 +87,6 @@ type Provider struct {
ClientSecret string
ProviderURL string
Scopes []string
SessionLifetimeTTL time.Duration
// Some providers, such as google, require additional remote api calls to retrieve
// user details like groups. Provider is responsible for parsing.
@ -160,7 +159,6 @@ func (p *Provider) Authenticate(code string) (*sessions.SessionState, error) {
AccessToken: oauth2Token.AccessToken,
RefreshToken: oauth2Token.RefreshToken,
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
Email: claims.Email,
User: idToken.Subject,
Groups: claims.Groups,

View file

@ -210,7 +210,6 @@ func TestCookieStore_SaveSession(t *testing.T) {
&SessionState{
AccessToken: "token1234",
RefreshToken: "refresh4321",
LifetimeDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
Email: "user@domain.com",
User: "user",
@ -219,7 +218,6 @@ func TestCookieStore_SaveSession(t *testing.T) {
&SessionState{
AccessToken: "token1234",
RefreshToken: "refresh4321",
LifetimeDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
Email: "user@domain.com",
User: "user",

View file

@ -1,7 +1,11 @@
package sessions // import "github.com/pomerium/pomerium/internal/sessions"
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/pomerium/pomerium/internal/cryptutil"
@ -17,18 +21,14 @@ type SessionState struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token"`
RefreshDeadline time.Time `json:"refresh_deadline"`
LifetimeDeadline time.Time `json:"lifetime_deadline"`
Email string `json:"email"`
User string `json:"user"` // 'sub' in jwt
User string `json:"user"`
Groups []string `json:"groups"`
}
// LifetimePeriodExpired returns true if the lifetime has expired
func (s *SessionState) LifetimePeriodExpired() bool {
return isExpired(s.LifetimeDeadline)
ImpersonateEmail string
ImpersonateGroups []string
}
// RefreshPeriodExpired returns true if the refresh period has expired
@ -36,6 +36,28 @@ func (s *SessionState) RefreshPeriodExpired() bool {
return isExpired(s.RefreshDeadline)
}
type idToken struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Expiry jsonTime `json:"exp"`
IssuedAt jsonTime `json:"iat"`
Nonce string `json:"nonce"`
AtHash string `json:"at_hash"`
}
// IssuedAt parses the IDToken's issue date and returns a valid go time.Time.
func (s *SessionState) IssuedAt() (time.Time, error) {
payload, err := parseJWT(s.IDToken)
if err != nil {
return time.Time{}, fmt.Errorf("internal/sessions: malformed jwt: %v", err)
}
var token idToken
if err := json.Unmarshal(payload, &token); err != nil {
return time.Time{}, fmt.Errorf("internal/sessions: failed to unmarshal claims: %v", err)
}
return time.Time(token.IssuedAt), nil
}
func isExpired(t time.Time) bool {
return t.Before(time.Now())
}
@ -61,3 +83,37 @@ func UnmarshalSession(value string, c cryptutil.Cipher) (*SessionState, error) {
func ExtendDeadline(ttl time.Duration) time.Time {
return time.Now().Add(ttl).Truncate(time.Second)
}
func parseJWT(p string) ([]byte, error) {
parts := strings.Split(p, ".")
if len(parts) < 2 {
return nil, fmt.Errorf("internal/sessions: malformed jwt, expected 3 parts got %d", len(parts))
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("internal/sessions: malformed jwt payload: %v", err)
}
return payload, nil
}
type jsonTime time.Time
func (j *jsonTime) UnmarshalJSON(b []byte) error {
var n json.Number
if err := json.Unmarshal(b, &n); err != nil {
return err
}
var unix int64
if t, err := n.Int64(); err == nil {
unix = t
} else {
f, err := n.Float64()
if err != nil {
return err
}
unix = int64(f)
}
*j = jsonTime(time.Unix(unix, 0))
return nil
}

View file

@ -18,7 +18,6 @@ func TestSessionStateSerialization(t *testing.T) {
want := &SessionState{
AccessToken: "token1234",
RefreshToken: "refresh4321",
LifetimeDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
Email: "user@domain.com",
User: "user",
@ -45,16 +44,10 @@ func TestSessionStateExpirations(t *testing.T) {
session := &SessionState{
AccessToken: "token1234",
RefreshToken: "refresh4321",
LifetimeDeadline: time.Now().Add(-1 * time.Hour),
RefreshDeadline: time.Now().Add(-1 * time.Hour),
Email: "user@domain.com",
User: "user",
}
if !session.LifetimePeriodExpired() {
t.Errorf("expected lifetime period to be expired")
}
if !session.RefreshPeriodExpired() {
t.Errorf("expected lifetime period to be expired")
}
@ -80,3 +73,30 @@ func TestExtendDeadline(t *testing.T) {
})
}
}
func TestSessionState_IssuedAt(t *testing.T) {
t.Parallel()
tests := []struct {
name string
IDToken string
want time.Time
wantErr bool
}{
{"simple parse", "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlkzYm1qV3R4US16OW1fM1RLb0dtRWciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY3MjY4NywiZXhwIjoxNTU4Njc2Mjg3fQ.a4g8W94E7iVJhiIUmsNMwJssfx3Evi8sXeiXgXMC7kHNvftQ2CFU_LJ-dqZ5Jf61OXcrp26r7lUcTNENXuen9tyUWAiHvxk6OHTxZusdywTCY5xowpSZBO9PDWYrmmdvfhRbaKO6QVAUMkbKr1Tr8xqfoaYVXNZhERXhcVReDznI0ccbwCGrNx5oeqiL4eRdZY9eqFXi4Yfee0mkef9oyVPc2HvnpwcpM0eckYa_l_ZQChGjXVGBFIus_Ao33GbWDuc9gs-_Vp2ev4KUT2qWb7AXMCGDLx0tWI9umm7mCBi_7xnaanGKUYcVwcSrv45arllAAwzuNxO0BVw3oRWa5Q", time.Unix(1558672687, 0), false},
{"bad jwt", "x.x.x-x-x", time.Time{}, true},
{"malformed jwt", "x", time.Time{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &SessionState{IDToken: tt.IDToken}
got, err := s.IssuedAt()
if (err != nil) != tt.wantErr {
t.Errorf("SessionState.IssuedAt() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("SessionState.IssuedAt() = %v, want %v", got.Format(time.RFC3339), tt.want.Format(time.RFC3339))
}
})
}
}

View file

@ -1,10 +1,7 @@
package templates // import "github.com/pomerium/pomerium/internal/templates"
import (
"fmt"
"html/template"
"github.com/pomerium/pomerium/internal/version"
)
// New loads html and style resources directly. Panics on failure.
@ -17,129 +14,240 @@ func New() *template.Template {
* {
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: none;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,'Helvetica Neue', sans-serif;
font-size: 15px;
line-height: 1.4em;
}
body {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 1em;
line-height: 1.42857143;
color: #333;
background: #f0f0f0;
display: flex;
flex-direction: row;
align-items: center;
background: #F8F8FF;
}
p {
margin: 1.5em 0;
}
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
.container {
max-width: 40em;
display: block;
margin: 10% auto;
#main {
width: 100%;
height: 100vh;
text-align: center;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.content, .message, button {
border: 1px solid rgba(0,0,0,.125);
border-bottom-width: 4px;
#info-box {
max-width: 480px;
width: 480px;
margin-top: 200px;
margin-right: auto;
margin-bottom: 0px;
margin-left: auto;
justify-content: center;
flex-grow: 1;
}
section {
display: flex;
flex-direction: column;
position: relative;
text-align: left;
}
h1 {
font-size: 36px;
font-weight: 400;
text-align: center;
letter-spacing: 0.3px;
text-transform: uppercase;
color: #32325d;
}
h1.title {
text-align: center;
background: #F8F8FF;
margin: 15px 0;
}
h2 {
margin: 15px 0;
color: #32325d;
text-transform: uppercase;
letter-spacing: 0.3px;
font-size: 18px;
font-weight: 650;
padding-top: 20px;
}
.card {
margin: 0 -30px;
padding: 20px 30px 30px;
border-radius: 4px;
border: 1px solid #e8e8fb;
background-color: #F8F8FF;
}
.content, .message {
background-color: #fff;
padding: 2rem;
margin: 1rem 0;
fieldset {
margin-bottom: 20px;
background: #FCFCFF;
box-shadow: 0 1px 3px 0 rgba(50, 50, 93, 0.15), 0 4px 6px 0 rgba(112, 157, 199, 0.15);
border-radius: 4px;
border: none;
font-size: 0;
}
.error, .message {
border-bottom-color: #c00;
fieldset label {
position: relative;
display: flex;
flex-direction: row;
height: 42px;
padding: 10px 0;
align-items: center;
justify-content: center;
font-weight: 400;
}
.message {
padding: 1.5rem 2rem 1.3rem;
fieldset label:not(:last-child) {
border-bottom: 1px solid #f0f5fa;
}
header {
border-bottom: 1px solid rgba(0,0,0,.075);
margin: -2rem 0 2rem;
padding: 2rem 0 1.8rem;
}
header h1 {
font-size: 1.5em;
font-weight: normal;
}
.error header {
color: #c00;
}
.details {
font-size: .85rem;
color: #999;
}
button {
color: #fff;
background-color: #3B8686;
cursor: pointer;
font-size: 1.5rem;
font-weight: bold;
padding: 1rem 2.5rem;
text-shadow: 0 3px 1px rgba(0,0,0,.2);
outline: none;
}
button:active {
border-top-width: 4px;
border-bottom-width: 1px;
text-shadow: none;
}
footer {
font-size: 0.75em;
color: #999;
fieldset label span {
min-width: 125px;
padding: 0 15px;
text-align: right;
margin: 1rem;
}
#group {
display: flex;
align-items: center;
}
#group::before {
display: inline-flex;
content: '';
height: 15px;
background-position: -1000px -1000px;
background-repeat: no-repeat;
// margin-right: 10px;
}
.icon {
display: inline-table;
margin-top: -72px;
background: #F8F8FF;
text-align: center;
width: 75px;
height: auto;
}
.logo {
width: 115px;
height: auto;
}
.ok{
fill: #6E43E8;
}
.error{
fill: #EB292F;
}
p.message {
margin-top: 10px;
margin-bottom: 10px;
}
.field {
flex: 1;
padding: 0 15px;
background: transparent;
font-weight: 400;
color: #31325f;
outline: none;
cursor: text;
}
fieldset .select::after {
content: '';
position: absolute;
width: 9px;
height: 5px;
right: 20px;
top: 50%;
margin-top: -2px;
pointer-events: none;
background: #6E43E8 url("data:image/svg+xml;utf8,<svg viewBox='0 0 140 140' width='24' height='24' xmlns='http://www.w3.org/2000/svg'><g><path d='m121.3,34.6c-1.6-1.6-4.2-1.6-5.8,0l-51,51.1-51.1-51.1c-1.6-1.6-4.2-1.6-5.8,0-1.6,1.6-1.6,4.2 0,5.8l53.9,53.9c0.8,0.8 1.8,1.2 2.9,1.2 1,0 2.1-0.4 2.9-1.2l53.9-53.9c1.7-1.6 1.7-4.2 0.1-5.8z' fill='white'/></g></svg>") no-repeat;
}
input {
flex: 1;
border-style: none;
outline: none;
color: #313b3f;
}
select {
flex: 1;
border-style: none;
outline: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
outline: none;
color: #313b3f;
cursor: pointer;
background: transparent;
}
.flex{
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.button {
color: #FCFCFF;
background: #6E43E8;
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
border-radius: 4px;
border: 0;
font-weight: 700;
width: 50%;
height: 40px;
outline: none;
cursor: pointer;
padding: 10px;
text-decoration: none;
}
.button.half{
flex-grow:0;
flex-shrink:0;
flex-basis:calc(50% - 10px);
}
.button.full{
flex-grow:1;
}
.button:hover {
transform: translateY(-1px);
box-shadow: 0 7px 14px 0 rgba(50, 50, 93, 0.1), 0 3px 6px 0 rgba(0, 0, 0, 0.08);
}
.off-color{
background: #5735B5;
}
</style>
{{end}}`))
t = template.Must(t.Parse(fmt.Sprintf(`{{define "footer.html"}}Secured by <b>pomerium</b> %s {{end}}`, version.FullVersion())))
t = template.Must(t.Parse(`
{{define "sign_in_message.html"}}
{{if eq (len .AllowedDomains) 1}}
{{if eq (index .AllowedDomains 0) "@*"}}
<p>You may sign in with any {{.ProviderName}} account.</p>
{{else}}
<p>You may sign in with your <b>{{index .AllowedDomains 0}}</b> {{.ProviderName}} account.</p>
{{end}}
{{else if gt (len .AllowedDomains) 1}}
<p>
You may sign in with any of these {{.ProviderName}} accounts:<br>
{{range $i, $e := .AllowedDomains}}{{if $i}}, {{end}}<b>{{$e}}</b>{{end}}
</p>
{{end}}
{{end}}`))
t = template.Must(t.Parse(`
{{define "sign_in.html"}}
<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
<title>Sign In</title>
{{template "header.html"}}
</head>
<body>
<div class="container">
<div class="content">
<header>
<h1>Sign in to <b>{{.Destination}}</b></h1>
</header>
{{template "sign_in_message.html" .}}
<form method="GET" action="/start">
<input type="hidden" name="redirect_uri" value="{{.Redirect}}">
<button type="submit" class="btn">Sign in with {{.ProviderName}}</button>
</form>
</div>
<footer>{{template "footer.html"}} </footer>
</div>
</body>
</html>
{{end}}`))
template.Must(t.Parse(`
@ -147,49 +255,114 @@ footer {
<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
<title>Error</title>
<title>{{.Code}} - {{.Title}}</title>
{{template "header.html"}}
</head>
<body>
<div class="container">
<div class="content error">
<header>
<h1>{{.Title}}</h1>
</header>
<p>
{{.Message}}<br>
<span class="details">HTTP {{.Code}}</span>
</p>
</div>
<footer>{{template "footer.html"}} </footer>
</div>
</body>
</html>{{end}}`))
t = template.Must(t.Parse(`
{{define "sign_out.html"}}
<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
<title>Sign Out</title>
{{template "header.html"}}
</head>
<body>
<div class="container">
<div class="content">
<header>
<h1>Sign out of <b>{{.Destination}}</b></h1>
</header>
<p>You're currently signed in as <b>{{.Email}}</b>. This will also sign you out of other internal apps.</p>
<form method="POST" action="/sign_out">
<input type="hidden" name="redirect_uri" value="{{.Redirect}}">
<input type="hidden" name="sig" value="{{.Signature}}">
<input type="hidden" name="ts" value="{{.Timestamp}}">
<button type="submit">Sign out</button>
<div id="main">
<div id="info-box">
<div class="card">
<svg class="icon error" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z"/></svg>
<h1 class="title">{{.Title}}</h1>
<section>
<p class="message">{{.Message}}.</p>
<p class="message">Troubleshoot your <a href="/.pomerium">session</a>.</p>
</section>
</form>
</div>
<footer>{{template "footer.html"}}</footer>
</div>
<footer>
<a href="https://www.pomerium.io" style="display: block;">
<svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 139 30"><defs><style>.a {fill: #6e43e8;}.a,.b {fill-rule: evenodd;}.b,.c {fill: #fff;}</style></defs><title>powered-by-pomerium</title><path class="a" d="M10.6,5.5H138.4c3.09,0,5.6,2,5.6,4.39V31.11c0,2.42-2.51,4.39-5.6,4.39H10.6c-3.09,0-5.6-2-5.6-4.39V9.89C5,7.47,7.51,5.5,10.6,5.5Z" transform="translate(-5 -5.5)" /><path class="b" d="M75.4,26.62H73.94l1.13-2.79-2.25-5.69h1.54L75.78,22l1.43-3.87h1.54Zm-5.61-2.44a2.42,2.42,0,0,1-1.5-.55V24H66.78V15.56h1.51v3a2.48,2.48,0,0,1,1.5-.55c1.58,0,2.66,1.28,2.66,3.09S71.37,24.18,69.79,24.18Zm-.32-4.88a1.68,1.68,0,0,0-1.18.53v2.52a1.65,1.65,0,0,0,1.18.54c.85,0,1.44-.73,1.44-1.8S70.32,19.3,69.47,19.3Zm-8.8,4.33a2.38,2.38,0,0,1-1.5.55c-1.57,0-2.66-1.27-2.66-3.09S57.6,18,59.17,18a2.44,2.44,0,0,1,1.5.55v-3h1.52V24H60.67Zm0-3.8a1.63,1.63,0,0,0-1.17-.53c-.86,0-1.45.73-1.45,1.79s.59,1.8,1.45,1.8a1.6,1.6,0,0,0,1.17-.54Zm-9,1.68A1.69,1.69,0,0,0,53.47,23a3.55,3.55,0,0,0,1.76-.56v1.26a4.73,4.73,0,0,1-2,.46,3,3,0,0,1-3-3.13A2.87,2.87,0,0,1,53.11,18,2.66,2.66,0,0,1,55.7,21a5.53,5.53,0,0,1,0,.56Zm1.37-2.34a1.38,1.38,0,0,0-1.37,1.36h2.57A1.28,1.28,0,0,0,53.05,19.17Zm-5.34.93V24H46.2v-5.9h1.51v.59A2,2,0,0,1,49.16,18a1.65,1.65,0,0,1,.49.06v1.35a1.83,1.83,0,0,0-.53-.07A1.87,1.87,0,0,0,47.71,20.1ZM41,21.51A1.69,1.69,0,0,0,42.76,23a3.55,3.55,0,0,0,1.76-.56v1.26a4.73,4.73,0,0,1-2,.46,3,3,0,0,1-3-3.13A2.87,2.87,0,0,1,42.4,18,2.66,2.66,0,0,1,45,21a5.53,5.53,0,0,1,0,.56Zm1.37-2.34A1.38,1.38,0,0,0,41,20.53h2.57A1.28,1.28,0,0,0,42.34,19.17ZM35.7,24l-1.2-4-1.2,4H32l-2-5.9h1.51l1.19,4,1.19-4h1.37l1.19,4,1.19-4h1.51l-2,5.9Zm-9.23.14a2.94,2.94,0,0,1-3-3.09,3,3,0,1,1,6.07,0A2.94,2.94,0,0,1,26.47,24.18Zm0-4.92c-.88,0-1.49.75-1.49,1.83s.61,1.83,1.49,1.83S28,22.18,28,21.09,27.35,19.26,26.47,19.26Zm-6.62,1.87H18.49V24H17V15.93h2.87a2.61,2.61,0,1,1,0,5.2Zm-.22-4H18.49V19.9h1.14a1.38,1.38,0,1,0,0-2.75Z" transform="translate(-5 -5.5)" /><path class="c" d="M132.71,14.9A3.93,3.93,0,0,0,128.78,11H94.59a3.93,3.93,0,0,0-3.93,3.92V31.06h2.71V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2.47ZM93.37,19a5.49,5.49,0,1,1,11,0Zm12.95,0a5.49,5.49,0,1,1,11,0Zm12.94,0a5.49,5.49,0,1,1,11,0Z" transform="translate(-5 -5.5)" /></svg>
</a>
</footer>
</div>
</body>
</html>
{{end}}`))
t = template.Must(t.Parse(`
{{define "dashboard.html"}}
<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
<title>Pomerium</title>
{{template "header.html"}}
</head>
<body>
<div id="main">
<div id="info-box">
<div class="card">
<svg class="icon ok" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
</svg>
<form method="POST" action="{{.SignoutURL}}">
<section>
<h2>Session</h2>
<p class="message">Your current session details.</p>
<fieldset>
<label>
<span>Email</span>
<input name="email" type="email" class="field" value="{{.Email}}" disabled>
</label>
<label>
<span>User</span>
<input name="user" type="text" class="field" value="{{.User}}" disabled>
</label>
<label class="select">
<span>Groups</span>
<div id="group" class="field">
<select name="group">
{{range .Groups}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
</div>
</label>
<label>
<span>Expiry</span>
<input name="session expiration" type="text" class="field" value="{{.RefreshDeadline}}" disabled>
</label>
</fieldset>
</section>
<div class="flex">
<button class="button half" type="submit">Sign Out</button>
<a href="/.pomerium/refresh" class="button half">Refresh</a>
</div>
</form>
{{if .IsAdmin}}
<form method="POST" action="/.pomerium/impersonate">
<section>
<h2>Sign-in-as</h2>
<p class="message">Administrators can temporarily impersonate another a user.</p>
<fieldset>
<label>
<span>Email</span>
<input name="email" type="email" class="field" value="{{.ImpersonateEmail}}" placeholder="user@example.com">
</label>
<label>
<span>Group</span>
<input name="group" type="text" class="field" value="{{.ImpersonateGroup}}" placeholder="engineering">
</label>
</fieldset>
</section>
<div class="flex">
<input name="csrf" type="hidden" value="{{.CSRF}}">
<button class="button full" type="submit">Impersonate session</button>
</div>
</form>
{{ end }}
</div>
</div>
<footer>
<a href="https://www.pomerium.io" style="display: block;">
<svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 139 30"><defs><style>.a {fill: #6e43e8;}.a,.b {fill-rule: evenodd;}.b,.c {fill: #fff;}</style></defs><title>powered-by-pomerium</title><path class="a" d="M10.6,5.5H138.4c3.09,0,5.6,2,5.6,4.39V31.11c0,2.42-2.51,4.39-5.6,4.39H10.6c-3.09,0-5.6-2-5.6-4.39V9.89C5,7.47,7.51,5.5,10.6,5.5Z" transform="translate(-5 -5.5)" /><path class="b" d="M75.4,26.62H73.94l1.13-2.79-2.25-5.69h1.54L75.78,22l1.43-3.87h1.54Zm-5.61-2.44a2.42,2.42,0,0,1-1.5-.55V24H66.78V15.56h1.51v3a2.48,2.48,0,0,1,1.5-.55c1.58,0,2.66,1.28,2.66,3.09S71.37,24.18,69.79,24.18Zm-.32-4.88a1.68,1.68,0,0,0-1.18.53v2.52a1.65,1.65,0,0,0,1.18.54c.85,0,1.44-.73,1.44-1.8S70.32,19.3,69.47,19.3Zm-8.8,4.33a2.38,2.38,0,0,1-1.5.55c-1.57,0-2.66-1.27-2.66-3.09S57.6,18,59.17,18a2.44,2.44,0,0,1,1.5.55v-3h1.52V24H60.67Zm0-3.8a1.63,1.63,0,0,0-1.17-.53c-.86,0-1.45.73-1.45,1.79s.59,1.8,1.45,1.8a1.6,1.6,0,0,0,1.17-.54Zm-9,1.68A1.69,1.69,0,0,0,53.47,23a3.55,3.55,0,0,0,1.76-.56v1.26a4.73,4.73,0,0,1-2,.46,3,3,0,0,1-3-3.13A2.87,2.87,0,0,1,53.11,18,2.66,2.66,0,0,1,55.7,21a5.53,5.53,0,0,1,0,.56Zm1.37-2.34a1.38,1.38,0,0,0-1.37,1.36h2.57A1.28,1.28,0,0,0,53.05,19.17Zm-5.34.93V24H46.2v-5.9h1.51v.59A2,2,0,0,1,49.16,18a1.65,1.65,0,0,1,.49.06v1.35a1.83,1.83,0,0,0-.53-.07A1.87,1.87,0,0,0,47.71,20.1ZM41,21.51A1.69,1.69,0,0,0,42.76,23a3.55,3.55,0,0,0,1.76-.56v1.26a4.73,4.73,0,0,1-2,.46,3,3,0,0,1-3-3.13A2.87,2.87,0,0,1,42.4,18,2.66,2.66,0,0,1,45,21a5.53,5.53,0,0,1,0,.56Zm1.37-2.34A1.38,1.38,0,0,0,41,20.53h2.57A1.28,1.28,0,0,0,42.34,19.17ZM35.7,24l-1.2-4-1.2,4H32l-2-5.9h1.51l1.19,4,1.19-4h1.37l1.19,4,1.19-4h1.51l-2,5.9Zm-9.23.14a2.94,2.94,0,0,1-3-3.09,3,3,0,1,1,6.07,0A2.94,2.94,0,0,1,26.47,24.18Zm0-4.92c-.88,0-1.49.75-1.49,1.83s.61,1.83,1.49,1.83S28,22.18,28,21.09,27.35,19.26,26.47,19.26Zm-6.62,1.87H18.49V24H17V15.93h2.87a2.61,2.61,0,1,1,0,5.2Zm-.22-4H18.49V19.9h1.14a1.38,1.38,0,1,0,0-2.75Z" transform="translate(-5 -5.5)" /><path class="c" d="M132.71,14.9A3.93,3.93,0,0,0,128.78,11H94.59a3.93,3.93,0,0,0-3.93,3.92V31.06h2.71V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2.47ZM93.37,19a5.49,5.49,0,1,1,11,0Zm12.95,0a5.49,5.49,0,1,1,11,0Zm12.94,0a5.49,5.49,0,1,1,11,0Z" transform="translate(-5 -5.5)" /></svg>
</a>
</footer>
</div>
</body>
</html>

View file

@ -1,21 +0,0 @@
- from: httpbin.corp.beyondperimeter.com
to: http://httpbin
allowed_domains:
- pomerium.io
cors_allow_preflight: true
timeout: 30s
- 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

@ -35,7 +35,7 @@ func (m *AuthenticateRequest) Reset() { *m = AuthenticateRequest{} }
func (m *AuthenticateRequest) String() string { return proto.CompactTextString(m) }
func (*AuthenticateRequest) ProtoMessage() {}
func (*AuthenticateRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_authenticate_2c495f1e6e8d5900, []int{0}
return fileDescriptor_authenticate_d9796afa57ba1f78, []int{0}
}
func (m *AuthenticateRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_AuthenticateRequest.Unmarshal(m, b)
@ -73,7 +73,7 @@ func (m *ValidateRequest) Reset() { *m = ValidateRequest{} }
func (m *ValidateRequest) String() string { return proto.CompactTextString(m) }
func (*ValidateRequest) ProtoMessage() {}
func (*ValidateRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_authenticate_2c495f1e6e8d5900, []int{1}
return fileDescriptor_authenticate_d9796afa57ba1f78, []int{1}
}
func (m *ValidateRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ValidateRequest.Unmarshal(m, b)
@ -111,7 +111,7 @@ func (m *ValidateReply) Reset() { *m = ValidateReply{} }
func (m *ValidateReply) String() string { return proto.CompactTextString(m) }
func (*ValidateReply) ProtoMessage() {}
func (*ValidateReply) Descriptor() ([]byte, []int) {
return fileDescriptor_authenticate_2c495f1e6e8d5900, []int{2}
return fileDescriptor_authenticate_d9796afa57ba1f78, []int{2}
}
func (m *ValidateReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ValidateReply.Unmarshal(m, b)
@ -146,7 +146,6 @@ type Session struct {
Email string `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"`
Groups []string `protobuf:"bytes,6,rep,name=groups,proto3" json:"groups,omitempty"`
RefreshDeadline *timestamp.Timestamp `protobuf:"bytes,7,opt,name=refresh_deadline,json=refreshDeadline,proto3" json:"refresh_deadline,omitempty"`
LifetimeDeadline *timestamp.Timestamp `protobuf:"bytes,8,opt,name=lifetime_deadline,json=lifetimeDeadline,proto3" json:"lifetime_deadline,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@ -156,7 +155,7 @@ func (m *Session) Reset() { *m = Session{} }
func (m *Session) String() string { return proto.CompactTextString(m) }
func (*Session) ProtoMessage() {}
func (*Session) Descriptor() ([]byte, []int) {
return fileDescriptor_authenticate_2c495f1e6e8d5900, []int{3}
return fileDescriptor_authenticate_d9796afa57ba1f78, []int{3}
}
func (m *Session) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Session.Unmarshal(m, b)
@ -225,13 +224,6 @@ func (m *Session) GetRefreshDeadline() *timestamp.Timestamp {
return nil
}
func (m *Session) GetLifetimeDeadline() *timestamp.Timestamp {
if m != nil {
return m.LifetimeDeadline
}
return nil
}
func init() {
proto.RegisterType((*AuthenticateRequest)(nil), "authenticate.AuthenticateRequest")
proto.RegisterType((*ValidateRequest)(nil), "authenticate.ValidateRequest")
@ -377,32 +369,31 @@ var _Authenticator_serviceDesc = grpc.ServiceDesc{
Metadata: "authenticate.proto",
}
func init() { proto.RegisterFile("authenticate.proto", fileDescriptor_authenticate_2c495f1e6e8d5900) }
func init() { proto.RegisterFile("authenticate.proto", fileDescriptor_authenticate_d9796afa57ba1f78) }
var fileDescriptor_authenticate_2c495f1e6e8d5900 = []byte{
// 378 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x92, 0xcf, 0x6e, 0x9b, 0x40,
0x10, 0xc6, 0x8d, 0xff, 0x81, 0xc7, 0x58, 0x76, 0xa7, 0x7f, 0x44, 0xa9, 0xaa, 0xda, 0xf4, 0xe2,
0x56, 0x15, 0x96, 0xdc, 0x53, 0x8f, 0x95, 0x5a, 0x25, 0xca, 0x91, 0x58, 0xb9, 0x5a, 0x18, 0xc6,
0xf6, 0x2a, 0x98, 0x25, 0xec, 0x12, 0xc9, 0x2f, 0x97, 0x67, 0xc9, 0xa3, 0x44, 0x2c, 0x20, 0x43,
0x64, 0x2b, 0x37, 0x66, 0xf6, 0x37, 0x1f, 0xb3, 0xdf, 0xb7, 0x80, 0x7e, 0x26, 0xf7, 0x14, 0x4b,
0x16, 0xf8, 0x92, 0xdc, 0x24, 0xe5, 0x92, 0xa3, 0x59, 0xef, 0xd9, 0xdf, 0x76, 0x9c, 0xef, 0x22,
0x5a, 0xa8, 0xb3, 0x4d, 0xb6, 0x5d, 0x48, 0x76, 0x20, 0x21, 0xfd, 0x43, 0x52, 0xe0, 0xce, 0x0f,
0x78, 0xff, 0xb7, 0x36, 0xe0, 0xd1, 0x43, 0x46, 0x42, 0x22, 0x42, 0x37, 0xe0, 0x21, 0x59, 0xda,
0x54, 0x9b, 0x0f, 0x3c, 0xf5, 0xed, 0xfc, 0x82, 0xf1, 0x9d, 0x1f, 0xb1, 0xb0, 0x86, 0x7d, 0x06,
0x83, 0x85, 0x6b, 0xc9, 0xef, 0x29, 0x2e, 0x51, 0x9d, 0x85, 0xab, 0xbc, 0x74, 0x7e, 0xc2, 0xe8,
0x44, 0x27, 0xd1, 0x51, 0xb1, 0x62, 0xfd, 0x98, 0xf7, 0x14, 0x6b, 0x78, 0x3a, 0x13, 0x0a, 0x71,
0x9e, 0xda, 0xa0, 0xdf, 0x92, 0x10, 0x8c, 0xc7, 0x38, 0x03, 0xd3, 0x0f, 0x02, 0x12, 0xa2, 0x21,
0x3b, 0x2c, 0x7a, 0x4a, 0x1a, 0xbf, 0xc3, 0x28, 0xa5, 0x6d, 0x4a, 0x62, 0x5f, 0x32, 0x6d, 0xc5,
0x98, 0x65, 0xb3, 0x80, 0xea, 0xab, 0x75, 0x1a, 0xab, 0xe5, 0x97, 0xcb, 0x04, 0xa5, 0x56, 0xb7,
0xb8, 0x5c, 0xfe, 0x8d, 0x1f, 0xa0, 0x47, 0x07, 0x9f, 0x45, 0x56, 0x4f, 0x35, 0x8b, 0x02, 0x3f,
0x41, 0x7f, 0x97, 0xf2, 0x2c, 0x11, 0x56, 0x7f, 0xda, 0x99, 0x0f, 0xbc, 0xb2, 0xc2, 0xff, 0x30,
0xa9, 0x36, 0x08, 0xc9, 0x0f, 0x23, 0x16, 0x93, 0xa5, 0x4f, 0xb5, 0xf9, 0x70, 0x69, 0xbb, 0x85,
0xe3, 0x6e, 0xe5, 0xb8, 0xbb, 0xaa, 0x1c, 0xf7, 0xc6, 0xe5, 0xcc, 0xbf, 0x72, 0x04, 0xaf, 0xe0,
0x5d, 0xc4, 0xb6, 0x94, 0x67, 0x72, 0xd2, 0x31, 0xde, 0xd4, 0x99, 0x54, 0x43, 0x95, 0xd0, 0xf2,
0x59, 0x83, 0x51, 0x2d, 0x46, 0x9e, 0xe2, 0x0d, 0x98, 0xf5, 0x5c, 0x71, 0xe6, 0x36, 0xde, 0xca,
0x99, 0xcc, 0xed, 0x8f, 0x4d, 0xa4, 0x0c, 0xc4, 0x69, 0xe1, 0x35, 0x18, 0x55, 0x94, 0xf8, 0xb5,
0x09, 0xbd, 0x7a, 0x10, 0xf6, 0x97, 0x4b, 0xc7, 0x49, 0x74, 0x74, 0x5a, 0xf8, 0x07, 0x74, 0xaf,
0xf0, 0x00, 0xcf, 0xff, 0xed, 0xe2, 0x12, 0x9b, 0xbe, 0x32, 0xe2, 0xf7, 0x4b, 0x00, 0x00, 0x00,
0xff, 0xff, 0xcb, 0xcf, 0xe8, 0x63, 0xf4, 0x02, 0x00, 0x00,
var fileDescriptor_authenticate_d9796afa57ba1f78 = []byte{
// 354 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x91, 0x4d, 0x4f, 0xb3, 0x40,
0x14, 0x85, 0xcb, 0xdb, 0x0f, 0xda, 0x5b, 0x9a, 0xbe, 0xb9, 0x7e, 0x04, 0x31, 0xc6, 0x16, 0x37,
0xd5, 0x18, 0x9a, 0xd4, 0x95, 0x4b, 0x13, 0x4d, 0x8c, 0x4b, 0x6c, 0xdc, 0x36, 0x14, 0x6e, 0xdb,
0x89, 0x94, 0x41, 0x66, 0x30, 0xe9, 0xbf, 0xf5, 0x4f, 0xb8, 0x37, 0x0c, 0x10, 0xc1, 0xb4, 0x3b,
0xee, 0x99, 0xe7, 0x0e, 0x67, 0xce, 0x01, 0xf4, 0x52, 0xb9, 0xa1, 0x48, 0x32, 0xdf, 0x93, 0xe4,
0xc4, 0x09, 0x97, 0x1c, 0x8d, 0xaa, 0x66, 0x5d, 0xae, 0x39, 0x5f, 0x87, 0x34, 0x55, 0x67, 0xcb,
0x74, 0x35, 0x95, 0x6c, 0x4b, 0x42, 0x7a, 0xdb, 0x38, 0xc7, 0xed, 0x6b, 0x38, 0x7a, 0xa8, 0x2c,
0xb8, 0xf4, 0x91, 0x92, 0x90, 0x88, 0xd0, 0xf2, 0x79, 0x40, 0xa6, 0x36, 0xd2, 0x26, 0x3d, 0x57,
0x7d, 0xdb, 0xb7, 0x30, 0x7c, 0xf3, 0x42, 0x16, 0x54, 0xb0, 0x33, 0xe8, 0xb2, 0x60, 0x21, 0xf9,
0x3b, 0x45, 0x05, 0xaa, 0xb3, 0x60, 0x9e, 0x8d, 0xf6, 0x0d, 0x0c, 0x7e, 0xe9, 0x38, 0xdc, 0x29,
0x56, 0x2c, 0x3e, 0x33, 0x4d, 0xb1, 0x5d, 0x57, 0x67, 0x42, 0x21, 0xf6, 0xb7, 0x06, 0xfa, 0x2b,
0x09, 0xc1, 0x78, 0x84, 0x63, 0x30, 0x3c, 0xdf, 0x27, 0x21, 0x6a, 0xd7, 0xf6, 0x73, 0x4d, 0x5d,
0x8d, 0x57, 0x30, 0x48, 0x68, 0x95, 0x90, 0xd8, 0x14, 0xcc, 0x3f, 0xc5, 0x18, 0x85, 0x98, 0x43,
0x55, 0x6b, 0xcd, 0x9a, 0xb5, 0xec, 0x71, 0xa9, 0xa0, 0xc4, 0x6c, 0xe5, 0x8f, 0xcb, 0xbe, 0xf1,
0x18, 0xda, 0xb4, 0xf5, 0x58, 0x68, 0xb6, 0x95, 0x98, 0x0f, 0x78, 0x0a, 0x9d, 0x75, 0xc2, 0xd3,
0x58, 0x98, 0x9d, 0x51, 0x73, 0xd2, 0x73, 0x8b, 0x09, 0x9f, 0xe0, 0x7f, 0xe9, 0x20, 0x20, 0x2f,
0x08, 0x59, 0x44, 0xa6, 0x3e, 0xd2, 0x26, 0xfd, 0x99, 0xe5, 0xe4, 0x89, 0x3b, 0x65, 0xe2, 0xce,
0xbc, 0x4c, 0xdc, 0x1d, 0x16, 0x3b, 0x8f, 0xc5, 0xca, 0xec, 0x4b, 0x83, 0x41, 0x25, 0x7d, 0x9e,
0xe0, 0x0b, 0x18, 0xd5, 0x3a, 0x70, 0xec, 0xd4, 0x2a, 0xde, 0x53, 0x95, 0x75, 0x52, 0x47, 0x8a,
0x1c, 0xed, 0x06, 0x3e, 0x43, 0xb7, 0x6c, 0x00, 0x2f, 0xea, 0xd0, 0x9f, 0x1e, 0xad, 0xf3, 0x43,
0xc7, 0x71, 0xb8, 0xb3, 0x1b, 0x78, 0x0f, 0xba, 0x9b, 0x5b, 0xc7, 0xfd, 0x7f, 0x3b, 0x68, 0x62,
0xd9, 0x51, 0x39, 0xdc, 0xfd, 0x04, 0x00, 0x00, 0xff, 0xff, 0xdc, 0x47, 0xff, 0x7e, 0xab, 0x02,
0x00, 0x00,
}

View file

@ -23,5 +23,4 @@ message Session {
string email = 5;
repeated string groups = 6;
google.protobuf.Timestamp refresh_deadline = 7;
google.protobuf.Timestamp lifetime_deadline = 8;
}

View file

@ -12,10 +12,7 @@ func SessionFromProto(p *Session) (*sessions.SessionState, error) {
if p == nil {
return nil, fmt.Errorf("proto/authenticate: SessionFromProto session cannot be nil")
}
lifetimeDeadline, err := ptypes.Timestamp(p.LifetimeDeadline)
if err != nil {
return nil, fmt.Errorf("proto/authenticate: couldn't parse lifetime deadline %v", err)
}
refreshDeadline, err := ptypes.Timestamp(p.RefreshDeadline)
if err != nil {
return nil, fmt.Errorf("proto/authenticate: couldn't parse refresh deadline %v", err)
@ -28,7 +25,6 @@ func SessionFromProto(p *Session) (*sessions.SessionState, error) {
User: p.User,
Groups: p.Groups,
RefreshDeadline: refreshDeadline,
LifetimeDeadline: lifetimeDeadline,
}, nil
}
@ -37,10 +33,6 @@ func ProtoFromSession(s *sessions.SessionState) (*Session, error) {
if s == nil {
return nil, fmt.Errorf("proto/authenticate: ProtoFromSession session cannot be nil")
}
lifetimeDeadline, err := ptypes.TimestampProto(s.LifetimeDeadline)
if err != nil {
return nil, fmt.Errorf("proto/authenticate: couldn't parse lifetime deadline %v", err)
}
refreshDeadline, err := ptypes.TimestampProto(s.RefreshDeadline)
if err != nil {
return nil, fmt.Errorf("proto/authenticate: couldn't parse refresh deadline %v", err)
@ -53,6 +45,5 @@ func ProtoFromSession(s *sessions.SessionState) (*Session, error) {
User: s.User,
Groups: s.Groups,
RefreshDeadline: refreshDeadline,
LifetimeDeadline: lifetimeDeadline,
}, nil
}

View file

@ -23,70 +23,87 @@ var _ = math.Inf
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type AuthorizeRequest struct {
type Identity 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"`
// user context
ImpersonateEmail string `protobuf:"bytes,5,opt,name=impersonate_email,json=impersonateEmail,proto3" json:"impersonate_email,omitempty"`
ImpersonateGroups []string `protobuf:"bytes,6,rep,name=impersonate_groups,json=impersonateGroups,proto3" json:"impersonate_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 *Identity) Reset() { *m = Identity{} }
func (m *Identity) String() string { return proto.CompactTextString(m) }
func (*Identity) ProtoMessage() {}
func (*Identity) Descriptor() ([]byte, []int) {
return fileDescriptor_authorize_86f44c7874077551, []int{0}
}
func (m *AuthorizeRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_AuthorizeRequest.Unmarshal(m, b)
func (m *Identity) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Identity.Unmarshal(m, b)
}
func (m *AuthorizeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_AuthorizeRequest.Marshal(b, m, deterministic)
func (m *Identity) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Identity.Marshal(b, m, deterministic)
}
func (dst *AuthorizeRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_AuthorizeRequest.Merge(dst, src)
func (dst *Identity) XXX_Merge(src proto.Message) {
xxx_messageInfo_Identity.Merge(dst, src)
}
func (m *AuthorizeRequest) XXX_Size() int {
return xxx_messageInfo_AuthorizeRequest.Size(m)
func (m *Identity) XXX_Size() int {
return xxx_messageInfo_Identity.Size(m)
}
func (m *AuthorizeRequest) XXX_DiscardUnknown() {
xxx_messageInfo_AuthorizeRequest.DiscardUnknown(m)
func (m *Identity) XXX_DiscardUnknown() {
xxx_messageInfo_Identity.DiscardUnknown(m)
}
var xxx_messageInfo_AuthorizeRequest proto.InternalMessageInfo
var xxx_messageInfo_Identity proto.InternalMessageInfo
func (m *AuthorizeRequest) GetRoute() string {
func (m *Identity) GetRoute() string {
if m != nil {
return m.Route
}
return ""
}
func (m *AuthorizeRequest) GetUser() string {
func (m *Identity) GetUser() string {
if m != nil {
return m.User
}
return ""
}
func (m *AuthorizeRequest) GetEmail() string {
func (m *Identity) GetEmail() string {
if m != nil {
return m.Email
}
return ""
}
func (m *AuthorizeRequest) GetGroups() []string {
func (m *Identity) GetGroups() []string {
if m != nil {
return m.Groups
}
return nil
}
func (m *Identity) GetImpersonateEmail() string {
if m != nil {
return m.ImpersonateEmail
}
return ""
}
func (m *Identity) GetImpersonateGroups() []string {
if m != nil {
return m.ImpersonateGroups
}
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:"-"`
@ -98,7 +115,7 @@ 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}
return fileDescriptor_authorize_86f44c7874077551, []int{1}
}
func (m *AuthorizeReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_AuthorizeReply.Unmarshal(m, b)
@ -125,9 +142,48 @@ func (m *AuthorizeReply) GetIsValid() bool {
return false
}
type IsAdminReply struct {
IsAdmin bool `protobuf:"varint,1,opt,name=is_admin,json=isAdmin,proto3" json:"is_admin,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *IsAdminReply) Reset() { *m = IsAdminReply{} }
func (m *IsAdminReply) String() string { return proto.CompactTextString(m) }
func (*IsAdminReply) ProtoMessage() {}
func (*IsAdminReply) Descriptor() ([]byte, []int) {
return fileDescriptor_authorize_86f44c7874077551, []int{2}
}
func (m *IsAdminReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_IsAdminReply.Unmarshal(m, b)
}
func (m *IsAdminReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_IsAdminReply.Marshal(b, m, deterministic)
}
func (dst *IsAdminReply) XXX_Merge(src proto.Message) {
xxx_messageInfo_IsAdminReply.Merge(dst, src)
}
func (m *IsAdminReply) XXX_Size() int {
return xxx_messageInfo_IsAdminReply.Size(m)
}
func (m *IsAdminReply) XXX_DiscardUnknown() {
xxx_messageInfo_IsAdminReply.DiscardUnknown(m)
}
var xxx_messageInfo_IsAdminReply proto.InternalMessageInfo
func (m *IsAdminReply) GetIsAdmin() bool {
if m != nil {
return m.IsAdmin
}
return false
}
func init() {
proto.RegisterType((*AuthorizeRequest)(nil), "authorize.AuthorizeRequest")
proto.RegisterType((*Identity)(nil), "authorize.Identity")
proto.RegisterType((*AuthorizeReply)(nil), "authorize.AuthorizeReply")
proto.RegisterType((*IsAdminReply)(nil), "authorize.IsAdminReply")
}
// Reference imports to suppress errors if they are not otherwise used.
@ -142,7 +198,8 @@ const _ = grpc.SupportPackageIsVersion4
//
// 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)
Authorize(ctx context.Context, in *Identity, opts ...grpc.CallOption) (*AuthorizeReply, error)
IsAdmin(ctx context.Context, in *Identity, opts ...grpc.CallOption) (*IsAdminReply, error)
}
type authorizerClient struct {
@ -153,7 +210,7 @@ func NewAuthorizerClient(cc *grpc.ClientConn) AuthorizerClient {
return &authorizerClient{cc}
}
func (c *authorizerClient) Authorize(ctx context.Context, in *AuthorizeRequest, opts ...grpc.CallOption) (*AuthorizeReply, error) {
func (c *authorizerClient) Authorize(ctx context.Context, in *Identity, opts ...grpc.CallOption) (*AuthorizeReply, error) {
out := new(AuthorizeReply)
err := c.cc.Invoke(ctx, "/authorize.Authorizer/Authorize", in, out, opts...)
if err != nil {
@ -162,9 +219,19 @@ func (c *authorizerClient) Authorize(ctx context.Context, in *AuthorizeRequest,
return out, nil
}
func (c *authorizerClient) IsAdmin(ctx context.Context, in *Identity, opts ...grpc.CallOption) (*IsAdminReply, error) {
out := new(IsAdminReply)
err := c.cc.Invoke(ctx, "/authorize.Authorizer/IsAdmin", 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)
Authorize(context.Context, *Identity) (*AuthorizeReply, error)
IsAdmin(context.Context, *Identity) (*IsAdminReply, error)
}
func RegisterAuthorizerServer(s *grpc.Server, srv AuthorizerServer) {
@ -172,7 +239,7 @@ func RegisterAuthorizerServer(s *grpc.Server, srv AuthorizerServer) {
}
func _Authorizer_Authorize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AuthorizeRequest)
in := new(Identity)
if err := dec(in); err != nil {
return nil, err
}
@ -184,7 +251,25 @@ func _Authorizer_Authorize_Handler(srv interface{}, ctx context.Context, dec fun
FullMethod: "/authorize.Authorizer/Authorize",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthorizerServer).Authorize(ctx, req.(*AuthorizeRequest))
return srv.(AuthorizerServer).Authorize(ctx, req.(*Identity))
}
return interceptor(ctx, in, info, handler)
}
func _Authorizer_IsAdmin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Identity)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthorizerServer).IsAdmin(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/authorize.Authorizer/IsAdmin",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthorizerServer).IsAdmin(ctx, req.(*Identity))
}
return interceptor(ctx, in, info, handler)
}
@ -197,25 +282,34 @@ var _Authorizer_serviceDesc = grpc.ServiceDesc{
MethodName: "Authorize",
Handler: _Authorizer_Authorize_Handler,
},
{
MethodName: "IsAdmin",
Handler: _Authorizer_IsAdmin_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "authorize.proto",
}
func init() { proto.RegisterFile("authorize.proto", fileDescriptor_authorize_dad4e29706fc340b) }
func init() { proto.RegisterFile("authorize.proto", fileDescriptor_authorize_86f44c7874077551) }
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,
var fileDescriptor_authorize_86f44c7874077551 = []byte{
// 264 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x51, 0xbd, 0x4e, 0xc3, 0x30,
0x10, 0x6e, 0x68, 0x9b, 0x26, 0x27, 0xc4, 0xcf, 0x81, 0x20, 0x65, 0xaa, 0x3c, 0x15, 0x55, 0x74,
0x80, 0x89, 0x81, 0xa1, 0x03, 0x42, 0x5d, 0x33, 0xb0, 0x56, 0x46, 0xb1, 0xe0, 0xa4, 0x24, 0x8e,
0x6c, 0x07, 0xa9, 0x3c, 0x00, 0x8f, 0xc5, 0xb3, 0xa1, 0x5c, 0xd2, 0xc4, 0x48, 0x6c, 0xf9, 0x7e,
0x73, 0x77, 0x86, 0x53, 0x59, 0xbb, 0x0f, 0x6d, 0xe8, 0x4b, 0xad, 0x2b, 0xa3, 0x9d, 0xc6, 0xb8,
0x27, 0xc4, 0x4f, 0x00, 0xd1, 0x36, 0x53, 0xa5, 0x23, 0xb7, 0xc7, 0x4b, 0x98, 0x1a, 0x5d, 0x3b,
0x95, 0x04, 0x8b, 0x60, 0x19, 0xa7, 0x2d, 0x40, 0x84, 0x49, 0x6d, 0x95, 0x49, 0x8e, 0x98, 0xe4,
0xef, 0xc6, 0xa9, 0x0a, 0x49, 0x79, 0x32, 0x6e, 0x9d, 0x0c, 0xf0, 0x0a, 0xc2, 0x77, 0xa3, 0xeb,
0xca, 0x26, 0x93, 0xc5, 0x78, 0x19, 0xa7, 0x1d, 0xc2, 0x15, 0x9c, 0x53, 0x51, 0x29, 0x63, 0x75,
0x29, 0x9d, 0xda, 0xb5, 0xc9, 0x29, 0x27, 0xcf, 0x3c, 0xe1, 0x99, 0x4b, 0xee, 0x00, 0x7d, 0x73,
0x57, 0x18, 0x72, 0xa1, 0x5f, 0xf3, 0xc2, 0x82, 0x58, 0xc1, 0xc9, 0xe6, 0xb0, 0x4d, 0xaa, 0xaa,
0x7c, 0x8f, 0x73, 0x88, 0xc8, 0xee, 0x3e, 0x65, 0x4e, 0x19, 0x2f, 0x12, 0xa5, 0x33, 0xb2, 0xaf,
0x0d, 0x14, 0xb7, 0x70, 0xbc, 0xb5, 0x9b, 0xac, 0xa0, 0xd2, 0xb7, 0xca, 0x86, 0x18, 0xac, 0xac,
0xdf, 0x7f, 0x07, 0x00, 0x7d, 0xb1, 0xc1, 0x27, 0x88, 0x7b, 0x84, 0x17, 0xeb, 0xe1, 0xa2, 0x87,
0xe3, 0xdd, 0xcc, 0x3d, 0xf2, 0xef, 0x44, 0x62, 0x84, 0x8f, 0x30, 0xeb, 0x7e, 0xfc, 0x7f, 0xf8,
0xda, 0x27, 0xbd, 0x09, 0xc5, 0xe8, 0x2d, 0xe4, 0x37, 0x7b, 0xf8, 0x0d, 0x00, 0x00, 0xff, 0xff,
0x6d, 0x2f, 0xa0, 0x1b, 0xc6, 0x01, 0x00, 0x00,
}

View file

@ -3,16 +3,23 @@ syntax = "proto3";
package authorize;
service Authorizer {
rpc Authorize(AuthorizeRequest) returns (AuthorizeReply) {}
rpc Authorize(Identity) returns (AuthorizeReply) {}
rpc IsAdmin(Identity) returns (IsAdminReply) {}
}
message AuthorizeRequest {
message Identity {
// request context
string route = 1;
// user context
string user = 2;
string email = 3;
repeated string groups = 4;
// user context
string impersonate_email = 5;
repeated string impersonate_groups = 6;
}
message AuthorizeReply { bool is_valid = 1; }
message IsAdminReply { bool is_admin = 1; }

View file

@ -1,15 +1,15 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/pomerium/pomerium/proto/authorize (interfaces: AuthorizerClient)
// Source: proto/authorize/authorize.pb.go
// Package mock_authorize is a generated GoMock package.
package mock_authorize
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
authorize "github.com/pomerium/pomerium/proto/authorize"
"github.com/pomerium/pomerium/proto/authorize"
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
@ -37,10 +37,10 @@ func (m *MockAuthorizerClient) EXPECT() *MockAuthorizerClientMockRecorder {
}
// Authorize mocks base method
func (m *MockAuthorizerClient) Authorize(arg0 context.Context, arg1 *authorize.AuthorizeRequest, arg2 ...grpc.CallOption) (*authorize.AuthorizeReply, error) {
func (m *MockAuthorizerClient) Authorize(ctx context.Context, in *authorize.Identity, opts ...grpc.CallOption) (*authorize.AuthorizeReply, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs := []interface{}{ctx, in}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Authorize", varargs...)
@ -50,8 +50,81 @@ func (m *MockAuthorizerClient) Authorize(arg0 context.Context, arg1 *authorize.A
}
// Authorize indicates an expected call of Authorize
func (mr *MockAuthorizerClientMockRecorder) Authorize(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
func (mr *MockAuthorizerClientMockRecorder) Authorize(ctx, in interface{}, opts ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
varargs := append([]interface{}{ctx, in}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authorize", reflect.TypeOf((*MockAuthorizerClient)(nil).Authorize), varargs...)
}
// IsAdmin mocks base method
func (m *MockAuthorizerClient) IsAdmin(ctx context.Context, in *authorize.Identity, opts ...grpc.CallOption) (*authorize.IsAdminReply, error) {
m.ctrl.T.Helper()
varargs := []interface{}{ctx, in}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "IsAdmin", varargs...)
ret0, _ := ret[0].(*authorize.IsAdminReply)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IsAdmin indicates an expected call of IsAdmin
func (mr *MockAuthorizerClientMockRecorder) IsAdmin(ctx, in interface{}, opts ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx, in}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAdmin", reflect.TypeOf((*MockAuthorizerClient)(nil).IsAdmin), varargs...)
}
// MockAuthorizerServer is a mock of AuthorizerServer interface
type MockAuthorizerServer struct {
ctrl *gomock.Controller
recorder *MockAuthorizerServerMockRecorder
}
// MockAuthorizerServerMockRecorder is the mock recorder for MockAuthorizerServer
type MockAuthorizerServerMockRecorder struct {
mock *MockAuthorizerServer
}
// NewMockAuthorizerServer creates a new mock instance
func NewMockAuthorizerServer(ctrl *gomock.Controller) *MockAuthorizerServer {
mock := &MockAuthorizerServer{ctrl: ctrl}
mock.recorder = &MockAuthorizerServerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockAuthorizerServer) EXPECT() *MockAuthorizerServerMockRecorder {
return m.recorder
}
// Authorize mocks base method
func (m *MockAuthorizerServer) Authorize(arg0 context.Context, arg1 *authorize.Identity) (*authorize.AuthorizeReply, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Authorize", arg0, arg1)
ret0, _ := ret[0].(*authorize.AuthorizeReply)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Authorize indicates an expected call of Authorize
func (mr *MockAuthorizerServerMockRecorder) Authorize(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authorize", reflect.TypeOf((*MockAuthorizerServer)(nil).Authorize), arg0, arg1)
}
// IsAdmin mocks base method
func (m *MockAuthorizerServer) IsAdmin(arg0 context.Context, arg1 *authorize.Identity) (*authorize.IsAdminReply, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsAdmin", arg0, arg1)
ret0, _ := ret[0].(*authorize.IsAdminReply)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IsAdmin indicates an expected call of IsAdmin
func (mr *MockAuthorizerServerMockRecorder) IsAdmin(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAdmin", reflect.TypeOf((*MockAuthorizerServer)(nil).IsAdmin), arg0, arg1)
}

View file

@ -75,7 +75,6 @@ func TestProxy_Redeem(t *testing.T) {
IdToken: "mocked id token",
User: "user1",
Email: "test@email.com",
LifetimeDeadline: mockExpire,
RefreshDeadline: mockExpire,
}, nil)
tests := []struct {
@ -90,7 +89,6 @@ func TestProxy_Redeem(t *testing.T) {
IDToken: "mocked id token",
User: "user1",
Email: "test@email.com",
LifetimeDeadline: (fixedDate),
RefreshDeadline: (fixedDate),
}, false},
{"empty code", "", nil, true},
@ -172,7 +170,6 @@ func TestProxy_AuthenticateRefresh(t *testing.T) {
).Return(&pb.Session{
AccessToken: "new access token",
RefreshDeadline: mockExpire,
LifetimeDeadline: mockExpire,
}, nil).AnyTimes()
tests := []struct {
@ -186,7 +183,6 @@ func TestProxy_AuthenticateRefresh(t *testing.T) {
&sessions.SessionState{
AccessToken: "new access token",
RefreshDeadline: fixedDate,
LifetimeDeadline: fixedDate,
}, false},
{"empty refresh token", &sessions.SessionState{RefreshToken: ""}, nil, true},
}

View file

@ -13,8 +13,11 @@ import (
// Authorizer provides the authorize service interface
type Authorizer interface {
// Authorize takes a code and returns a validated session or an error
// Authorize takes a route and user session and returns whether the
// request is valid per access policy
Authorize(context.Context, string, *sessions.SessionState) (bool, error)
// IsAdmin takes a session and returns whether the user is an administrator
IsAdmin(context.Context, *sessions.SessionState) (bool, error)
// Close closes the auth connection if any.
Close() error
}
@ -35,29 +38,42 @@ func NewGRPCAuthorizeClient(opts *Options) (p *AuthorizeGRPC, err error) {
return &AuthorizeGRPC{Conn: conn, client: client}, nil
}
// AuthorizeGRPC is a gRPC implementation of an authenticator (authenticate client)
// AuthorizeGRPC is a gRPC implementation of an authenticator (authorize 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.
// Authorize takes a route and user session and returns whether the
// request is valid per access policy
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{
response, err := a.client.Authorize(ctx, &pb.Identity{
Route: route,
User: s.User,
Email: s.Email,
Groups: s.Groups,
ImpersonateEmail: s.ImpersonateEmail,
ImpersonateGroups: s.ImpersonateGroups,
})
return response.GetIsValid(), err
}
// IsAdmin takes a session and returns whether the user is an administrator
func (a *AuthorizeGRPC) IsAdmin(ctx context.Context, 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.IsAdmin(ctx, &pb.Identity{Email: s.Email, Groups: s.Groups})
return response.GetIsAdmin(), err
}
// Close tears down the ClientConn and all underlying connections.
func (a *AuthorizeGRPC) Close() error {
return a.Conn.Close()

View file

@ -43,3 +43,35 @@ func TestAuthorizeGRPC_Authorize(t *testing.T) {
})
}
}
func TestAuthorizeGRPC_IsAdmin(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mock.NewMockAuthorizerClient(ctrl)
client.EXPECT().IsAdmin(
gomock.Any(),
gomock.Any(),
).Return(&authorize.IsAdminReply{IsAdmin: true}, nil).AnyTimes()
tests := []struct {
name string
s *sessions.SessionState
want bool
wantErr bool
}{
{"good", &sessions.SessionState{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false},
{"session cannot be nil", nil, false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &AuthorizeGRPC{client: client}
got, err := a.IsAdmin(context.Background(), tt.s)
if (err != nil) != tt.wantErr {
t.Errorf("AuthorizeGRPC.IsAdmin() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("AuthorizeGRPC.IsAdmin() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -39,6 +39,8 @@ func (a MockAuthenticate) Close() error { return a.CloseError }
type MockAuthorize struct {
AuthorizeResponse bool
AuthorizeError error
IsAdminResponse bool
IsAdminError error
CloseError error
}
@ -49,3 +51,8 @@ func (a MockAuthorize) Close() error { return a.CloseError }
func (a MockAuthorize) Authorize(ctx context.Context, route string, s *sessions.SessionState) (bool, error) {
return a.AuthorizeResponse, a.AuthorizeError
}
// IsAdmin is a mocked IsAdmin function.
func (a MockAuthorize) IsAdmin(ctx context.Context, s *sessions.SessionState) (bool, error) {
return a.IsAdminResponse, a.IsAdminError
}

View file

@ -5,18 +5,15 @@ import (
"errors"
"reflect"
"testing"
"time"
"github.com/pomerium/pomerium/internal/sessions"
)
func TestMockAuthenticate(t *testing.T) {
// Absurd, but I caught a typo this way.
fixedDate := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
redeemResponse := &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: fixedDate,
}
ma := &MockAuthenticate{
RedeemError: errors.New("RedeemError"),
@ -24,7 +21,6 @@ func TestMockAuthenticate(t *testing.T) {
RefreshResponse: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: fixedDate,
},
RefreshError: errors.New("RefreshError"),
ValidateResponse: true,

View file

@ -5,7 +5,6 @@ import (
"fmt"
"net/http"
"net/url"
"reflect"
"strings"
"time"
@ -15,6 +14,7 @@ import (
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/policy"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/templates"
)
// StateParameter holds the redirect id along with the session id.
@ -33,11 +33,14 @@ func (p *Proxy) Handler() http.Handler {
}))
mux := http.NewServeMux()
mux.HandleFunc("/robots.txt", p.RobotsTxt)
mux.HandleFunc("/.pomerium/sign_out", p.SignOut)
mux.HandleFunc("/.pomerium/callback", p.OAuthCallback)
// mux.HandleFunc("/.pomerium/refresh", p.Refresh) //todo(bdd): needs DoS protection before inclusion
mux.HandleFunc("/", p.Proxy)
return validate.Then(mux)
mux.HandleFunc("/.pomerium", p.UserDashboard)
mux.HandleFunc("/.pomerium/impersonate", p.Impersonate) // POST
mux.HandleFunc("/.pomerium/sign_out", p.SignOutCallback)
// handlers handlers with validation
mux.Handle("/.pomerium/callback", validate.ThenFunc(p.OAuthCallback))
mux.Handle("/.pomerium/refresh", validate.ThenFunc(p.Refresh))
mux.Handle("/", validate.ThenFunc(p.Proxy))
return mux
}
// RobotsTxt sets the User-Agent header in the response to be "Disallow"
@ -46,17 +49,12 @@ func (p *Proxy) RobotsTxt(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
}
// SignOut redirects the request to the sign out url. It's the responsibility
// SignOutCallback redirects the request to the sign out url. It's the responsibility
// of the authenticate service to revoke the remote session and clear
// the local session state.
func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
redirectURL := &url.URL{
Scheme: "https",
Host: r.Host,
Path: "/",
}
fullURL := p.GetSignOutURL(p.AuthenticateURL, redirectURL)
http.Redirect(w, r, fullURL.String(), http.StatusFound)
func (p *Proxy) SignOutCallback(w http.ResponseWriter, r *http.Request) {
redirectURL := &url.URL{Scheme: "https", Host: r.Host, Path: "/"}
http.Redirect(w, r, redirectURL.String(), http.StatusFound)
}
// OAuthStart begins the authenticate flow, encrypting the redirect url
@ -65,34 +63,47 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) {
requestURI := r.URL.String()
callbackURL := p.GetRedirectURL(r.Host)
// state prevents cross site forgery and maintain state across the client and server
// CSRF value used to mitigate replay attacks.
state := &StateParameter{
SessionID: fmt.Sprintf("%x", cryptutil.GenerateKey()), // nonce
RedirectURI: requestURI, // where to redirect the user back to
SessionID: fmt.Sprintf("%x", cryptutil.GenerateKey()),
RedirectURI: requestURI,
}
// we encrypt this value to be opaque the browser cookie
// this value will be unique since we always use a randomized nonce as part of marshaling
encryptedCSRF, err := p.cipher.Marshal(state)
// Encrypt, and save CSRF state. Will be checked on callback.
localState, err := p.cipher.Marshal(state)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: failed to marshal csrf")
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return
}
p.csrfStore.SetCSRF(w, r, encryptedCSRF)
p.csrfStore.SetCSRF(w, r, localState)
// we encrypt this value to be opaque the uri query value
// this value will be unique since we always use a randomized nonce as part of marshaling
encryptedState, err := p.cipher.Marshal(state)
// Though the plaintext payload is identical, we re-encrypt which will
// create a different cipher text using another nonce
remoteState, err := p.cipher.Marshal(state)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: failed to encrypt cookie")
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return
}
signinURL := p.GetSignInURL(p.AuthenticateURL, callbackURL, encryptedState)
log.FromRequest(r).Info().Str("SigninURL", signinURL.String()).Msg("proxy: oauth start")
// redirect the user to the authenticate provider along with the encrypted state which
// contains a redirect uri pointing back to the proxy
// Sanity check. The encrypted payload of local and remote state should
// never match as each encryption round uses a cryptographic nonce.
//
// todo(bdd): since this should nearly (1/(2^32*2^32)) never happen should
// we panic as a failure most likely means the rands entropy source is failing?
if remoteState == localState {
p.sessionStore.ClearSession(w, r)
log.FromRequest(r).Error().Msg("proxy: encrypted state should not match")
httputil.ErrorResponse(w, r, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
signinURL := p.GetSignInURL(p.AuthenticateURL, callbackURL, remoteState)
log.FromRequest(r).Debug().Str("SigninURL", signinURL.String()).Msg("proxy: oauth start")
// Redirect the user to the authenticate service along with the encrypted
// state which contains a redirect uri back to the proxy and a nonce
http.Redirect(w, r, signinURL.String(), http.StatusFound)
}
@ -112,23 +123,18 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
httputil.ErrorResponse(w, r, errorString, http.StatusForbidden)
return
}
// We begin the process of redeeming the code for an access token.
session, err := p.AuthenticateClient.Redeem(r.Context(), r.Form.Get("code"))
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: error redeeming authorization code")
httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError)
return
}
encryptedState := r.Form.Get("state")
stateParameter := &StateParameter{}
err = p.cipher.Unmarshal(encryptedState, stateParameter)
// Encrypted CSRF passed from authenticate service
remoteStateEncrypted := r.Form.Get("state")
remoteStatePlain := new(StateParameter)
err = p.cipher.Unmarshal(remoteStateEncrypted, remoteStatePlain)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: could not unmarshal state")
httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError)
return
}
// Encrypted CSRF from session storage
c, err := p.csrfStore.GetCSRF(r)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: failed parsing csrf cookie")
@ -136,44 +142,34 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
return
}
p.csrfStore.ClearCSRF(w, r)
encryptedCSRF := c.Value
csrfParameter := &StateParameter{}
err = p.cipher.Unmarshal(encryptedCSRF, csrfParameter)
localStateEncrypted := c.Value
localStatePlain := new(StateParameter)
err = p.cipher.Unmarshal(localStateEncrypted, localStatePlain)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't unmarshal CSRF")
httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError)
return
}
if encryptedState == encryptedCSRF {
log.FromRequest(r).Error().Msg("encrypted state and CSRF should not be equal")
httputil.ErrorResponse(w, r, "Bad request", http.StatusBadRequest)
return
}
if !reflect.DeepEqual(stateParameter, csrfParameter) {
log.FromRequest(r).Error().Msg("state and CSRF should be equal")
httputil.ErrorResponse(w, r, "Bad request", http.StatusBadRequest)
// If the encrypted value of local and remote state match, reject.
// Likely a replay attack or nonce-reuse.
if remoteStateEncrypted == localStateEncrypted {
p.sessionStore.ClearSession(w, r)
log.FromRequest(r).Error().Msg("proxy: local and remote state should not match")
httputil.ErrorResponse(w, r, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// We store the session in a cookie and redirect the user back to the application
err = p.sessionStore.SaveSession(w, r, session)
if err != nil {
log.FromRequest(r).Error().Msg("error saving session")
httputil.ErrorResponse(w, r, "Error saving session", http.StatusInternalServerError)
// Decrypted remote and local state struct (inc. nonce) must match
if remoteStatePlain.SessionID != localStatePlain.SessionID {
p.sessionStore.ClearSession(w, r)
log.FromRequest(r).Error().Msg("proxy: CSRF mismatch")
httputil.ErrorResponse(w, r, "CSRF mismatch", http.StatusBadRequest)
return
}
log.FromRequest(r).Debug().
Str("code", r.Form.Get("code")).
Str("state", r.Form.Get("state")).
Str("RefreshToken", session.RefreshToken).
Str("session", session.AccessToken).
Str("RedirectURI", stateParameter.RedirectURI).
Msg("session")
// This is the redirect back to the original requested application
http.Redirect(w, r, stateParameter.RedirectURI, http.StatusFound)
http.Redirect(w, r, remoteStatePlain.RedirectURI, http.StatusFound)
}
// shouldSkipAuthentication contains conditions for skipping authentication.
@ -223,8 +219,7 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
}
}
err = p.authenticate(w, r, session)
if err != nil {
if err = p.authenticate(w, r, session); err != nil {
p.sessionStore.ClearSession(w, r)
log.Debug().Err(err).Msg("proxy: user unauthenticated")
httputil.ErrorResponse(w, r, "User unauthenticated", http.StatusForbidden)
@ -236,10 +231,6 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
httputil.ErrorResponse(w, r, "Access unauthorized", http.StatusForbidden)
return
}
// append
r.Header.Set(HeaderUserID, session.User)
r.Header.Set(HeaderEmail, session.Email)
r.Header.Set(HeaderGroups, strings.Join(session.Groups, ","))
}
// We have validated the users request and now proxy their request to the provided upstream.
@ -251,33 +242,162 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
route.ServeHTTP(w, r)
}
// Refresh refreshes a user session, validating group, extending timeout period, without requiring
// a user to re-authenticate
// func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) {
// session, err := p.sessionStore.LoadSession(r)
// if err != nil {
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
// return
// }
// session, err = p.AuthenticateClient.Refresh(r.Context(), session)
// if err != nil {
// log.FromRequest(r).Warn().Err(err).Msg("proxy: refresh failed")
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
// return
// }
// err = p.sessionStore.SaveSession(w, r, session)
// if err != nil {
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
// return
// }
// w.WriteHeader(http.StatusOK)
// jsonSession, err := json.Marshal(session)
// if err != nil {
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
// return
// }
// fmt.Fprint(w, string(jsonSession))
// }
// UserDashboard lets users investigate, and refresh their current session.
// It also contains certain administrative actions like user impersonation.
// Nota bene: This endpoint does authentication, not authorization.
func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) {
session, err := p.sessionStore.LoadSession(r)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: load session failed")
httputil.ErrorResponse(w, r, "", http.StatusBadRequest)
return
}
if err := p.authenticate(w, r, session); err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: authenticate failed")
httputil.ErrorResponse(w, r, "", http.StatusUnauthorized)
return
}
redirectURL := &url.URL{Scheme: "https", Host: r.Host, Path: "/.pomerium/sign_out"}
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: is admin client")
httputil.ErrorResponse(w, r, "", http.StatusInternalServerError)
return
}
// CSRF value used to mitigate replay attacks.
csrf := &StateParameter{SessionID: fmt.Sprintf("%x", cryptutil.GenerateKey())}
csrfCookie, err := p.cipher.Marshal(csrf)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: failed to marshal csrf")
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return
}
p.csrfStore.SetCSRF(w, r, csrfCookie)
t := struct {
Email string
User string
Groups []string
RefreshDeadline string
SignoutURL string
IsAdmin bool
ImpersonateEmail string
ImpersonateGroup string
CSRF string
}{
Email: session.Email,
User: session.User,
Groups: session.Groups,
RefreshDeadline: time.Until(session.RefreshDeadline).Round(time.Second).String(),
SignoutURL: p.GetSignOutURL(p.AuthenticateURL, redirectURL).String(),
IsAdmin: isAdmin,
ImpersonateEmail: session.ImpersonateEmail,
ImpersonateGroup: strings.Join(session.ImpersonateGroups[:], ","),
CSRF: csrf.SessionID,
}
templates.New().ExecuteTemplate(w, "dashboard.html", t)
return
}
// Refresh redeems and extends an existing authenticated oidc session with
// the underlying idenity provider. All session details including groups,
// timeouts, will be renewed.
func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) {
session, err := p.sessionStore.LoadSession(r)
if err != nil {
httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest)
return
}
iss, err := session.IssuedAt()
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't get token's create time")
httputil.ErrorResponse(w, r, "", http.StatusInternalServerError)
return
}
// reject a refresh if it's been less than 5 minutes to prevent a bad actor
// trying to DOS the identity provider.
if time.Since(iss) < p.refreshCooldown {
log.FromRequest(r).Error().Dur("cooldown", p.refreshCooldown).Err(err).Msg("proxy: refresh cooldown")
httputil.ErrorResponse(w, r,
fmt.Sprintf("Session must be %v old before refresh", p.refreshCooldown),
http.StatusBadRequest)
return
}
newSession, err := p.AuthenticateClient.Refresh(r.Context(), session)
if err != nil {
log.FromRequest(r).Warn().Err(err).Msg("proxy: refresh failed")
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return
}
if err = p.sessionStore.SaveSession(w, r, newSession); err != nil {
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/.pomerium", http.StatusFound)
}
// Impersonate takes the result of a form and adds user impersonation details
// to the user's current user sessions state if the user is currently an
// administrative user. Requests are redirected back to the user dashboard.
func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
if err := r.ParseForm(); err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: impersonate form")
httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest)
return
}
session, err := p.sessionStore.LoadSession(r)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: load session")
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return
}
// authorization check -- is this user an admin?
isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session)
if err != nil || !isAdmin {
log.FromRequest(r).Error().Err(err).Msg("proxy: user must be admin to impersonate")
httputil.ErrorResponse(w, r, "user must be admin to impersonate", http.StatusForbidden)
return
}
// CSRF check -- did this request originate from our form?
c, err := p.csrfStore.GetCSRF(r)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: failed parsing csrf cookie")
httputil.ErrorResponse(w, r, err.Error(), http.StatusBadRequest)
return
}
p.csrfStore.ClearCSRF(w, r)
encryptedCSRF := c.Value
decryptedCSRF := new(StateParameter)
if err = p.cipher.Unmarshal(encryptedCSRF, decryptedCSRF); err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: couldn't unmarshal CSRF")
httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError)
return
}
if decryptedCSRF.SessionID != r.FormValue("csrf") {
log.FromRequest(r).Error().Err(err).Msg("proxy: impersonate CSRF mismatch")
httputil.ErrorResponse(w, r, "CSRF mismatch", http.StatusForbidden)
return
}
// OK to impersonation
session.ImpersonateEmail = r.FormValue("email")
session.ImpersonateGroups = strings.Split(r.FormValue("group"), ",")
if err := p.sessionStore.SaveSession(w, r, session); err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: save session")
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return
}
}
http.Redirect(w, r, "/.pomerium", http.StatusFound)
}
// Authenticate authenticates a request by checking for a session cookie, and validating its expiration,
// clearing the session cookie if it's invalid and returning an error if necessary..
@ -297,6 +417,9 @@ func (p *Proxy) authenticate(w http.ResponseWriter, r *http.Request, session *se
return fmt.Errorf("proxy: session valid: %v : %v", valid, err)
}
}
r.Header.Set(HeaderUserID, session.User)
r.Header.Set(HeaderEmail, session.Email)
r.Header.Set(HeaderGroups, strings.Join(session.Groups, ","))
return nil
}

View file

@ -1,6 +1,7 @@
package proxy
import (
"bytes"
"errors"
"fmt"
"net/http"
@ -12,6 +13,7 @@ import (
"time"
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/policy"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/proxy/clients"
@ -32,7 +34,12 @@ func (a mockCipher) Decrypt(s []byte) ([]byte, error) {
}
return []byte("OK"), nil
}
func (a mockCipher) Marshal(s interface{}) (string, error) { return "ok", nil }
func (a mockCipher) Marshal(s interface{}) (string, error) {
if s == "error" {
return "", errors.New("error")
}
return "ok", nil
}
func (a mockCipher) Unmarshal(s string, i interface{}) error {
if string(s) == "unmarshal error" || string(s) == "error" {
return errors.New("error")
@ -153,9 +160,8 @@ func TestProxy_Signout(t *testing.T) {
t.Fatal(err)
}
req := httptest.NewRequest("GET", "/.pomerium/sign_out", nil)
rr := httptest.NewRecorder()
proxy.SignOut(rr, req)
proxy.SignOutCallback(rr, req)
if status := rr.Code; status != http.StatusFound {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusFound)
}
@ -201,78 +207,6 @@ func TestProxy_Handler(t *testing.T) {
}
}
func TestProxy_OAuthCallback(t *testing.T) {
normalSession := sessions.MockSessionStore{
Session: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(-10 * time.Second),
},
}
normalAuth := clients.MockAuthenticate{
RedeemResponse: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: time.Now().Add(10 * time.Second),
},
}
normalCsrf := sessions.MockCSRFStore{
ResponseCSRF: "ok",
GetError: nil,
Cookie: &http.Cookie{
Name: "something_csrf",
Value: "csrf_state",
}}
tests := []struct {
name string
csrf sessions.MockCSRFStore
session sessions.MockSessionStore
authenticator clients.MockAuthenticate
params map[string]string
wantCode int
}{
{"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},
{"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},
{"csrf err", sessions.MockCSRFStore{GetError: errors.New("error")}, normalSession, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusBadRequest},
{"unmarshal err", sessions.MockCSRFStore{
Cookie: &http.Cookie{
Name: "something_csrf",
Value: "unmarshal error",
},
}, normalSession, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError},
{"encrypted state != CSRF", normalCsrf, normalSession, normalAuth, map[string]string{"code": "code", "state": "csrf_state"}, http.StatusBadRequest},
{"session save err", normalCsrf, sessions.MockSessionStore{SaveError: errors.New("error")}, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
proxy, err := New(testOptions())
if err != nil {
t.Fatal(err)
}
proxy.sessionStore = &tt.session
proxy.csrfStore = tt.csrf
proxy.AuthenticateClient = tt.authenticator
proxy.cipher = mockCipher{}
// proxy.Csrf
req := httptest.NewRequest(http.MethodPost, "/.pomerium/callback", nil)
q := req.URL.Query()
for k, v := range tt.params {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
w := httptest.NewRecorder()
proxy.OAuthCallback(w, req)
if status := w.Code; status != tt.wantCode {
t.Errorf("handler returned wrong status code: got %v want %v", status, tt.wantCode)
}
})
}
}
func Test_extendDeadline(t *testing.T) {
tests := []struct {
name string
@ -333,7 +267,6 @@ func TestProxy_Proxy(t *testing.T) {
goodSession := &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(10 * time.Second),
}
@ -374,13 +307,14 @@ func TestProxy_Proxy(t *testing.T) {
{"unknown host", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound},
{"user forbidden", opts, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
// authenticate errors
{"no session error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: http.ErrNoCookie, Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound},
{"weird load session error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: errors.New("weird"), Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusInternalServerError},
{"failed refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RefreshError: errors.New("refresh error")}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden},
{"cannot resave refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{SaveError: errors.New("weird"), Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden},
{"authenticate validation error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: false}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden},
{"public access", optsPublic, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK},
{"public access, but unknown host", optsPublic, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
// no session, redirect to login
{"no http found (no session)", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: http.ErrNoCookie}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest},
}
for _, tt := range tests {
@ -410,3 +344,152 @@ func TestProxy_Proxy(t *testing.T) {
})
}
}
func TestProxy_UserDashboard(t *testing.T) {
opts := testOptions()
tests := []struct {
name string
options *config.Options
method string
cipher cryptutil.Cipher
session sessions.SessionStore
authenticator clients.Authenticator
authorizer clients.Authorizer
wantAdminForm bool
wantStatus int
}{
{"good", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, false, http.StatusOK},
{"cannot load session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("load error")}, clients.MockAuthenticate{}, clients.MockAuthorize{}, false, http.StatusBadRequest},
{"auth failure, validation error", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", RefreshDeadline: time.Now().Add(10 * time.Second)}}, clients.MockAuthenticate{ValidateError: errors.New("not valid anymore"), ValidateResponse: false}, clients.MockAuthorize{}, false, http.StatusUnauthorized},
{"can't save csrf", opts, http.MethodGet, &cryptutil.MockCipher{MarshalError: errors.New("err")}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, false, http.StatusInternalServerError},
{"want admin form good admin authorization", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, true, http.StatusOK},
{"is admin but authorization fails", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminError: errors.New("err")}, false, http.StatusInternalServerError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p, err := New(tt.options)
if err != nil {
t.Fatal(err)
}
p.cipher = tt.cipher
p.sessionStore = tt.session
p.AuthenticateClient = tt.authenticator
p.AuthorizeClient = tt.authorizer
r := httptest.NewRequest(tt.method, "/", nil)
w := httptest.NewRecorder()
p.UserDashboard(w, r)
if status := w.Code; status != tt.wantStatus {
t.Errorf("status code: got %v want %v", status, tt.wantStatus)
t.Errorf("\n%+v", opts)
}
if adminForm := strings.Contains(w.Body.String(), "impersonate"); adminForm != tt.wantAdminForm {
t.Errorf("wanted admin form got %v want %v", adminForm, tt.wantAdminForm)
t.Errorf("\n%+v", w.Body.String())
}
})
}
}
func TestProxy_Refresh(t *testing.T) {
opts := testOptions()
opts.RefreshCooldown = 0
timeSinceError := testOptions()
timeSinceError.RefreshCooldown = time.Duration(int(^uint(0) >> 1))
tests := []struct {
name string
options *config.Options
method string
cipher cryptutil.Cipher
session sessions.SessionStore
authenticator clients.Authenticator
authorizer clients.Authorizer
wantStatus int
}{
{"good", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusFound},
{"cannot load session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("load error")}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusBadRequest},
{"bad id token", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "bad"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusInternalServerError},
{"issue date too soon", timeSinceError, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusBadRequest},
{"refresh failure", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{RefreshError: errors.New("err")}, clients.MockAuthorize{}, http.StatusInternalServerError},
{"can't save refreshed session", opts, http.MethodGet, &cryptutil.MockCipher{}, &sessions.MockSessionStore{SaveError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlppQ1g0WndDYl9tcUVxM2xnbmFZRHciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY1NDEzNywiZXhwIjoxNTU4NjU3NzM3fQ.Flah31XfqmPhWYh2rJ-6rtowmSQFgp6HqDf1rpS38Wo0DXnIYmXxEQVLanDNV62Z0sLhUk1QO9NqoSgA3NscM-Ww-JsqU80oKnWcMYweUb_KU0kfHyTiUB0iEHMqu6tXn5dA_dIaPnL5oorXZ_gG4sooRxBZrDkaNAjRINLciKDQkUTVaNfnM6IBZ_pWDPd2lWGtj8h8sEIe2PIiH73Z2VLlXz8kw60VTPsi9U2zrF0ZJ9MfRGJhceQ58vW2ZlFfXJixgvbOZjKmcRv8NaJDIUss48l0Bsya6icZ0l1ZK-sAiFr0KVLTl2ywu8d5SQpTJ1X7vDW_u_04xaqDQUdYKA"}}, clients.MockAuthenticate{}, clients.MockAuthorize{}, http.StatusInternalServerError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p, err := New(tt.options)
if err != nil {
t.Fatal(err)
}
p.cipher = tt.cipher
p.sessionStore = tt.session
p.AuthenticateClient = tt.authenticator
p.AuthorizeClient = tt.authorizer
r := httptest.NewRequest(tt.method, "/", nil)
w := httptest.NewRecorder()
p.Refresh(w, r)
if status := w.Code; status != tt.wantStatus {
t.Errorf("status code: got %v want %v", status, tt.wantStatus)
// t.Errorf("\n%+v", w.Body.String())
t.Errorf("\n%+v", opts)
}
})
}
}
func TestProxy_Impersonate(t *testing.T) {
opts := testOptions()
tests := []struct {
name string
options *config.Options
method string
email string
groups string
csrf string
cipher cryptutil.Cipher
sessionStore sessions.SessionStore
csrfStore sessions.CSRFStore
authenticator clients.Authenticator
authorizer clients.Authorizer
wantStatus int
}{
{"good", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusFound},
{"session load error", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{LoadError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
{"non admin users rejected", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: false}, http.StatusForbidden},
{"non admin users rejected on error", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true, IsAdminError: errors.New("err")}, http.StatusForbidden},
{"csrf from store retrieve failure", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}, GetError: errors.New("err")}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusBadRequest},
{"can't decrypt csrf value", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{UnmarshalError: errors.New("err")}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
{"decrypted csrf mismatch", opts, http.MethodPost, "user@blah.com", "", "CSRF!", &cryptutil.MockCipher{}, &sessions.MockSessionStore{Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusForbidden},
{"save session failure", opts, http.MethodPost, "user@blah.com", "", "", &cryptutil.MockCipher{}, &sessions.MockSessionStore{SaveError: errors.New("err"), Session: &sessions.SessionState{Email: "user@test.example", IDToken: ""}}, &sessions.MockCSRFStore{Cookie: &http.Cookie{Value: "csrf"}}, clients.MockAuthenticate{}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p, err := New(tt.options)
if err != nil {
t.Fatal(err)
}
p.cipher = tt.cipher
p.sessionStore = tt.sessionStore
p.csrfStore = tt.csrfStore
p.AuthenticateClient = tt.authenticator
p.AuthorizeClient = tt.authorizer
postForm := url.Values{}
postForm.Add("email", tt.email)
postForm.Add("group", tt.groups)
postForm.Set("csrf", tt.csrf)
r := httptest.NewRequest(tt.method, "/", bytes.NewBufferString(postForm.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
w := httptest.NewRecorder()
p.Impersonate(w, r)
if status := w.Code; status != tt.wantStatus {
t.Errorf("status code: got %v want %v", status, tt.wantStatus)
// t.Errorf("\n%+v", w.Body.String())
t.Errorf("\n%+v", opts)
}
})
}
}

View file

@ -10,6 +10,7 @@ import (
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/pomerium/pomerium/internal/config"
@ -95,6 +96,7 @@ type Proxy struct {
redirectURL *url.URL
templates *template.Template
routeConfigs map[string]*routeConfig
refreshCooldown time.Duration
}
type routeConfig struct {
@ -143,6 +145,7 @@ func New(opts *config.Options) (*Proxy, error) {
SharedKey: opts.SharedKey,
redirectURL: &url.URL{Path: "/.pomerium/callback"},
templates: templates.New(),
refreshCooldown: opts.RefreshCooldown,
}
for _, route := range opts.Policies {

View file

@ -1,6 +1,7 @@
package proxy // import "github.com/pomerium/pomerium/proxy"
import (
"errors"
"io/ioutil"
"net"
"net/http"
@ -10,6 +11,8 @@ import (
"time"
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/proxy/clients"
"github.com/pomerium/pomerium/internal/policy"
)
@ -237,3 +240,46 @@ func TestNew(t *testing.T) {
})
}
}
func TestProxy_OAuthCallback(t *testing.T) {
tests := []struct {
name string
csrf sessions.MockCSRFStore
session sessions.MockSessionStore
authenticator clients.MockAuthenticate
params map[string]string
wantCode int
}{
{"good", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusFound},
{"error", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"error": "some error"}, http.StatusForbidden},
{"state err", sessions.MockCSRFStore{ResponseCSRF: "ok", GetError: nil, Cookie: &http.Cookie{Name: "something_csrf", Value: "csrf_state"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "error"}, http.StatusInternalServerError},
{"csrf err", sessions.MockCSRFStore{GetError: errors.New("error")}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusBadRequest},
{"unmarshal err", sessions.MockCSRFStore{Cookie: &http.Cookie{Name: "something_csrf", Value: "unmarshal error"}}, sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RedeemResponse: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken"}}, map[string]string{"code": "code", "state": "state"}, http.StatusInternalServerError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
proxy, err := New(testOptions())
if err != nil {
t.Fatal(err)
}
proxy.sessionStore = &tt.session
proxy.csrfStore = tt.csrf
proxy.AuthenticateClient = tt.authenticator
proxy.cipher = mockCipher{}
// proxy.Csrf
req := httptest.NewRequest(http.MethodPost, "/.pomerium/callback", nil)
q := req.URL.Query()
for k, v := range tt.params {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
w := httptest.NewRecorder()
proxy.OAuthCallback(w, req)
if status := w.Code; status != tt.wantCode {
t.Errorf("handler returned wrong status code: got %v want %v", status, tt.wantCode)
}
})
}
}