diff --git a/CHANGELOG.md b/CHANGELOG.md index ae374551f..995589d4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Pomerium Changelog +## vUNRELEASED + +### NEW + +- Add programmatic authentication support. [GH-177] + +### CHANGED + +### FIXED + ## v0.0.5 ### NEW diff --git a/authenticate/authenticate.go b/authenticate/authenticate.go index cc9ea31dc..3d42dd377 100644 --- a/authenticate/authenticate.go +++ b/authenticate/authenticate.go @@ -8,7 +8,6 @@ import ( "net/url" "github.com/pomerium/pomerium/internal/config" - "github.com/pomerium/pomerium/internal/cryptutil" "github.com/pomerium/pomerium/internal/identity" "github.com/pomerium/pomerium/internal/sessions" @@ -49,6 +48,7 @@ type Authenticate struct { templates *template.Template csrfStore sessions.CSRFStore sessionStore sessions.SessionStore + restStore sessions.SessionStore cipher cryptutil.Cipher provider identity.Authenticator } @@ -71,7 +71,6 @@ func New(opts config.Options) (*Authenticate, error) { CookieExpire: opts.CookieExpire, CookieCipher: cipher, }) - if err != nil { return nil, err } @@ -91,13 +90,17 @@ func New(opts config.Options) (*Authenticate, error) { if err != nil { return nil, err } - + restStore, err := sessions.NewRestStore(&sessions.RestStoreOptions{Cipher: cipher}) + if err != nil { + return nil, err + } return &Authenticate{ SharedKey: opts.SharedKey, RedirectURL: &redirectURL, templates: templates.New(), csrfStore: cookieStore, sessionStore: cookieStore, + restStore: restStore, cipher: cipher, provider: provider, }, nil diff --git a/authenticate/handlers.go b/authenticate/handlers.go index 659a8a477..0e9cb7ade 100644 --- a/authenticate/handlers.go +++ b/authenticate/handlers.go @@ -39,6 +39,8 @@ func (a *Authenticate) Handler() http.Handler { // authenticate-server endpoints mux.Handle("/sign_in", validate.ThenFunc(a.SignIn)) mux.Handle("/sign_out", validate.ThenFunc(a.SignOut)) // POST + // programmatic authentication endpoints + mux.Handle("/api/v1/token", c.ThenFunc(a.ExchangeToken)) return mux } @@ -74,12 +76,12 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) { if err != nil { switch err { case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession: - log.FromRequest(r).Debug().Err(err).Msg("proxy: invalid session") + log.FromRequest(r).Debug().Err(err).Msg("authenticate: invalid session") a.sessionStore.ClearSession(w, r) a.OAuthStart(w, r) return default: - log.FromRequest(r).Error().Err(err).Msg("proxy: unexpected error") + log.FromRequest(r).Error().Err(err).Msg("authenticate: unexpected error") httpErr := &httputil.Error{Message: "An unexpected error occurred", Code: http.StatusInternalServerError} httputil.ErrorResponse(w, r, httpErr) return @@ -137,7 +139,7 @@ func getAuthCodeRedirectURL(redirectURL *url.URL, state, authCode string) string func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { log.Error().Err(err).Msg("authenticate: error SignOut form") - httpErr := &httputil.Error{Code: http.StatusInternalServerError} + httpErr := &httputil.Error{Code: http.StatusBadRequest} httputil.ErrorResponse(w, r, httpErr) return } @@ -237,11 +239,10 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) if code == "" { log.FromRequest(r).Error().Msg("authenticate: provider missing code") return "", httputil.Error{Code: http.StatusBadRequest, Message: "Missing Code"} - } // validate the returned code with the identity provider - session, err := a.provider.Authenticate(code) + session, err := a.provider.Authenticate(r.Context(), code) if err != nil { log.FromRequest(r).Error().Err(err).Msg("authenticate: error redeeming authenticate code") return "", httputil.Error{Code: http.StatusInternalServerError, Message: err.Error()} @@ -275,11 +276,39 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) return "", httputil.Error{Code: http.StatusBadRequest, Message: "Invalid Redirect URI domain"} } - err = a.sessionStore.SaveSession(w, r, session) - if err != nil { + if err := a.sessionStore.SaveSession(w, r, session); err != nil { log.Error().Err(err).Msg("authenticate: failed saving new session") return "", httputil.Error{Code: http.StatusInternalServerError, Message: "Internal Error"} } return redirect, nil } + +// ExchangeToken takes an identity provider issued JWT as input ('id_token) +// and exchanges that token for a pomerium session. The provided token's +// audience ('aud') attribute must match Pomerium's client_id. +func (a *Authenticate) ExchangeToken(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusInternalServerError, Message: err.Error()}) + return + } + code := r.Form.Get("id_token") + if code == "" { + log.FromRequest(r).Error().Msg("authenticate: provider missing id token") + httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusBadRequest, Message: "missing id token"}) + return + } + session, err := a.provider.IDTokenToSession(r.Context(), code) + if err != nil { + log.FromRequest(r).Error().Err(err).Msg("authenticate: error exchanging identity provider code") + httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusInternalServerError, Message: "could not exchange identity for session"}) + return + } + log.Info().Interface("session", session).Msg("Session") + if err := a.restStore.SaveSession(w, r, session); err != nil { + log.Error().Err(err).Msg("authenticate: failed returning new session") + httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusInternalServerError, Message: "authenticate: failed returning new session"}) + return + } + return +} diff --git a/authenticate/handlers_test.go b/authenticate/handlers_test.go index f4f0e9cfc..5fea3ca21 100644 --- a/authenticate/handlers_test.go +++ b/authenticate/handlers_test.go @@ -272,40 +272,10 @@ func TestAuthenticate_SignOut(t *testing.T) { wantCode int wantBody string }{ - {"good post", - http.MethodPost, - "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.StatusFound, - ""}, - {"failed revoke", - http.MethodPost, - "https://corp.pomerium.io/", - "sig", - "ts", - identity.MockProvider{RevokeError: errors.New("OH NO")}, - &sessions.MockSessionStore{ - Session: &sessions.SessionState{ - AccessToken: "AccessToken", - RefreshToken: "RefreshToken", - Email: "blah@blah.com", - - RefreshDeadline: time.Now().Add(10 * time.Second), - }, - }, - http.StatusBadRequest, - "could not revoke"}, + {"good post", http.MethodPost, "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.StatusFound, ""}, + {"failed revoke", http.MethodPost, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &sessions.MockSessionStore{Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, http.StatusBadRequest, "could not revoke"}, + {"malformed form", http.MethodPost, "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.StatusBadRequest, ""}, + {"load session error", http.MethodPost, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{}, &sessions.MockSessionStore{LoadError: errors.New("hi"), Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", Email: "blah@blah.com", RefreshDeadline: time.Now().Add(10 * time.Second)}}, http.StatusFound, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -321,7 +291,9 @@ func TestAuthenticate_SignOut(t *testing.T) { params.Add("ts", tt.ts) params.Add("redirect_uri", tt.redirectURL) u.RawQuery = params.Encode() - + if tt.name == "malformed form" { + u.RawQuery = "example=%zzzzz" + } r := httptest.NewRequest(tt.method, u.String(), nil) w := httptest.NewRecorder() @@ -678,3 +650,50 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) { }) } } + +func TestAuthenticate_ExchangeToken(t *testing.T) { + cipher := &cryptutil.MockCipher{} + tests := []struct { + name string + method string + idToken string + restStore sessions.SessionStore + cipher cryptutil.Cipher + provider identity.MockProvider + want string + }{ + {"good", http.MethodPost, "token", &sessions.RestStore{Cipher: cipher}, cipher, identity.MockProvider{IDTokenToSessionResponse: sessions.SessionState{IDToken: "ok"}}, ""}, + {"could not exchange identity for session", http.MethodPost, "token", &sessions.RestStore{Cipher: cipher}, cipher, identity.MockProvider{IDTokenToSessionError: errors.New("error")}, "could not exchange identity for session"}, + {"missing token", http.MethodPost, "", &sessions.RestStore{Cipher: cipher}, cipher, identity.MockProvider{IDTokenToSessionResponse: sessions.SessionState{IDToken: "ok"}}, "missing id token"}, + {"save error", http.MethodPost, "token", &sessions.MockSessionStore{SaveError: errors.New("error")}, cipher, identity.MockProvider{IDTokenToSessionResponse: sessions.SessionState{IDToken: "ok"}}, "failed returning new session"}, + {"malformed form", http.MethodPost, "token", &sessions.RestStore{Cipher: cipher}, cipher, identity.MockProvider{IDTokenToSessionResponse: sessions.SessionState{IDToken: "ok"}}, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Authenticate{ + restStore: tt.restStore, + cipher: tt.cipher, + provider: tt.provider, + } + form := url.Values{} + if tt.idToken != "" { + form.Add("id_token", tt.idToken) + } + rawForm := form.Encode() + + if tt.name == "malformed form" { + rawForm = "example=%zzzzz" + } + r := httptest.NewRequest(tt.method, "/", strings.NewReader(rawForm)) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + w := httptest.NewRecorder() + + a.ExchangeToken(w, r) + got := w.Body.String() + if !strings.Contains(got, tt.want) { + t.Errorf("Authenticate.ExchangeToken() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index ad9f198f9..450f88839 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -53,7 +53,7 @@ function docsSidebar(title) { { title, collapsable: false, - children: ["", "identity-providers", "signed-headers", "certificates", "examples", "impersonation", "upgrading"] + children: ["", "identity-providers", "signed-headers", "certificates", "examples", "impersonation", "programmatic-access", "upgrading"] } ]; } diff --git a/docs/docs/programmatic-access.md b/docs/docs/programmatic-access.md new file mode 100644 index 000000000..641eddb3b --- /dev/null +++ b/docs/docs/programmatic-access.md @@ -0,0 +1,69 @@ +--- +title: Programmatic access +description: >- + This article describes how to configure pomerium to be used to enable + machine-to-machine programmatic access. +--- + +# Programmatic access + +This page describes how to access Pomerium endpoints programmatically. + +## Configuration + +Every identity provider has slightly different methods for issuing OAuth 2.0 access tokens [suitable][proof key for code exchange] for machine-to-machine use, please review your identity provider's documentation. For example: + +- [Google Oauth2 2.0 for Desktop Apps](https://developers.google.com/identity/protocols/OAuth2InstalledApp) +- [Okta PKCE Flow](https://developer.okta.com/docs/concepts/auth-overview/#authorization-code-flow) +- [Azure Active Directory using the OAuth 2.0 code grant flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code) + +For the sake of illustration, this guide and example scripts will use Google as the underlying identity provider. + +### Identity Provider Configuration + +To configure programmatic access for Pomerium we'll need to set up **an additional** OAuth 2.0 client ID that can issue `id_tokens` whose [audience](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation) matches the Client ID of Pomerium. Follow these instructions adapted from [Google's documentation](https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_desktop_app): + +1. Go to the [Credentials page](https://console.cloud.google.com/apis/credentials). +2. Select the project with the Pomerium secured resource. +3. Click **Create credentials**, then select **OAuth Client ID**. +4. Under **Application type**, select **Other**, add a **Name**, then click **Create**. +5. On the OAuth client window that appears, note the **client ID** and **client secret**. +6. On the **Credentials** window, your new **Other** credentials appear along with the primary client ID that's used to access your application. + +### High level flow + +The application interacting with Pomerium will roughly have to manage the following access flow. + +1. A user authenticates with the OpenID Connect identity provider. This typically requires handling the [Proof Key for Code Exchange] process. +2. Exchange the code from the [Proof Key for Code Exchange] for a valid `refresh_token`. +3. Using the `refresh_token` from the last step, request the identity provider issue a new `id_token` which has our Pomerium app's `client_id` as the `audience`. +4. Exchange the identity provider issued `id_token` for a `pomerium` token (e.g. `https://authenticate.{your-domain}/api/v1/token`). +5. Use the pomerium issued `Token` [authorization bearer token] for all requests to Pomerium protected endpoints until it's `Expiry`. Authorization policy will be tied to the user as normal. + +### Expiration and revocation + +Your application should handle token expiration. If the session expires before work is done, the identity provider issued `refresh_token` can be used to create a new valid session by repeating steps 3 and on. + +Also, you should write your code to anticipate the possibility that a granted `refresh_token` may stop working. For example, a refresh token might stop working if the underlying user changes passwords, revokes access, or if the administrator removes rotates or deletes the OAuth Client ID. + +## Example Code + +It's not as bad as it sounds. Please see the following minimal but complete examples. + +### Python + +```bash +python scripts/programmatic_access.py --client-secret REPLACE_ME \ + --client-id 851877082059-85tfqg9hlm8j9km5d9uripd0dvk72mvk.apps.googleusercontent.com \ + --pomerium-client-id 851877082059-bfgkpj09noog7as3gpc3t7r6n9sjbgs6.apps.googleusercontent.com +``` + +<<< @/scripts/programmatic_access.py + +### Bash + +<<< @/scripts/programmatic_access.sh + +[authorization bearer token]: https://developers.google.com/gmail/markup/actions/verifying-bearer-tokens +[identity provider]: ../docs/identity-providers.md +[proof key for code exchange]: https://tools.ietf.org/html/rfc7636 diff --git a/go.mod b/go.mod index 075f11811..461ae3780 100644 --- a/go.mod +++ b/go.mod @@ -3,24 +3,30 @@ module github.com/pomerium/pomerium go 1.12 require ( + cloud.google.com/go v0.40.0 // indirect github.com/fsnotify/fsnotify v1.4.7 - github.com/golang/mock v1.2.0 + github.com/golang/mock v1.3.1 github.com/golang/protobuf v1.3.1 github.com/google/go-cmp v0.3.0 github.com/magiconair/properties v1.8.1 // indirect github.com/mitchellh/hashstructure v1.0.0 + github.com/pelletier/go-toml v1.4.0 // 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.14.3 - github.com/spf13/viper v1.3.2 + github.com/spf13/afero v1.2.2 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/viper v1.4.0 github.com/stretchr/testify v1.3.0 // indirect - golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f - golang.org/x/net v0.0.0-20190603091049-60506f45cf65 - 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 + go.opencensus.io v0.22.0 // indirect + golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 + golang.org/x/net v0.0.0-20190611141213-3f473d35a33a + golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae // indirect + google.golang.org/api v0.6.0 + google.golang.org/appengine v1.6.1 // indirect + google.golang.org/genproto v0.0.0-20190611190212-a7e196e89fd3 // indirect + google.golang.org/grpc v1.21.1 gopkg.in/square/go-jose.v2 v2.3.1 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index 4de46ba0c..6dcef31a4 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,79 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.40.0 h1:FjSY7bOj+WzJe6TZRVtXI2b9kAYvtNg4lMbcH2+MUkk= +cloud.google.com/go v0.40.0/go.mod h1:Tk58MuI9rbLMKlAjeO/bDnteAx7tX2gJIXw4T5Jwlro= 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/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 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 v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +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/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +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.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +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= @@ -43,9 +83,13 @@ github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9 github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +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/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg= +github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +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= @@ -53,96 +97,156 @@ github.com/pomerium/go-oidc v2.0.0+incompatible h1:gVvG/ExWsHQqatV+uceROnGmbVYF4 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/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +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-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/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/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/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/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 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/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +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/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= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +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-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/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/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-20190213061140-3a22650c66bd/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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190611141213-3f473d35a33a h1:+KkCgOMgnKSgenxTBoiwkMqTiouMIy/3o8RLdmSbGoY= +golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-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-20190227155943-e225da77a7e6/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-20180909124046-d0be0721c37e/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-20180905080454-ebe1bf3edb33/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-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/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae h1:xiXzMMEQdQcric9hXtr1QU98MHunKK7OTtsoU6bYWs4= +golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/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.1-0.20180807135948-17ff2d5776d2/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/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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-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-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 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= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.6.0 h1:2tJEkRfnZL5g1GeBUlITh/rqT5HG3sFcoVCUUxmgJ2g= +google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898 h1:yvw+zsSmSM02Z5H3ZdEV7B7Ql7eFrjQTnmByJvK+3J8= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= -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.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM= -google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190611190212-a7e196e89fd3 h1:0LGHEA/u5XLibPOx6D7D8FBT/ax6wT57vNKY0QckCwo= +google.golang.org/genproto v0.0.0-20190611190212-a7e196e89fd3/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= +google.golang.org/grpc v1.21.1/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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +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= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099 h1:XJP7lxbSxWLOMNdBE4B/STaqVy6L73o0knwj2vIlxnw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/identity/google.go b/internal/identity/google.go index 5db356c1f..bf8390a2b 100644 --- a/internal/identity/google.go +++ b/internal/identity/google.go @@ -33,7 +33,7 @@ type GoogleProvider struct { apiClient *admin.Service } -// NewGoogleProvider returns a new GoogleProvider and sets the provider url endpoints. +// NewGoogleProvider instantiates an OpenID Connect (OIDC) session with Google. func NewGoogleProvider(p *Provider) (*GoogleProvider, error) { ctx := context.Background() if p.ProviderURL == "" { @@ -123,33 +123,68 @@ func (p *GoogleProvider) Revoke(accessToken string) error { // cookies, re-authorization will not bring back refresh_token. A work around to this is to add // prompt=consent to the OAuth redirect URL and will always return a refresh_token. // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess -// https://developers.google.com/identity/protocols/OAuth2WebServer#offline -// https://stackoverflow.com/a/10857806/10592439 func (p *GoogleProvider) GetSignInURL(state string) string { - return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent")) + return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce) } // Authenticate creates an identity session with google from a authorization code, and follows up // call to the admin/group api to check what groups the user is in. -func (p *GoogleProvider) Authenticate(code string) (*sessions.SessionState, error) { - ctx := context.Background() +func (p *GoogleProvider) Authenticate(ctx context.Context, code string) (*sessions.SessionState, error) { // convert authorization code into a token oauth2Token, err := p.oauth.Exchange(ctx, code) if err != nil { return nil, fmt.Errorf("identity/google: token exchange failed %v", err) } - // id_token contains claims about the authenticated user + // id_token is a JWT that contains identity information about the user rawIDToken, ok := oauth2Token.Extra("id_token").(string) if !ok { return nil, fmt.Errorf("identity/google: response did not contain an id_token") } - // Parse and verify ID Token payload. + session, err := p.IDTokenToSession(ctx, rawIDToken) + if err != nil { + return nil, err + } + session.AccessToken = oauth2Token.AccessToken + session.RefreshToken = oauth2Token.RefreshToken + return session, nil +} + +// Refresh renews a user's session using an oidc refresh token withoutreprompting the user. +// Group membership is also refreshed. +// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens +func (p *GoogleProvider) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) { + if s.RefreshToken == "" { + return nil, errors.New("identity: missing refresh token") + } + t := oauth2.Token{RefreshToken: s.RefreshToken} + newToken, err := p.oauth.TokenSource(ctx, &t).Token() + if err != nil { + log.Error().Err(err).Msg("identity: refresh failed") + return nil, err + } + // id_token contains claims about the authenticated user + rawIDToken, ok := newToken.Extra("id_token").(string) + if !ok { + return nil, fmt.Errorf("identity/google: response did not contain an id_token") + } + newSession, err := p.IDTokenToSession(ctx, rawIDToken) + if err != nil { + return nil, err + } + newSession.AccessToken = newToken.AccessToken + newSession.RefreshToken = s.RefreshToken + return newSession, nil +} + +// IDTokenToSession takes an identity provider issued JWT as input ('id_token') +// and returns a session state. The provided token's audience ('aud') must +// match Pomerium's client_id. +func (p *GoogleProvider) IDTokenToSession(ctx context.Context, rawIDToken string) (*sessions.SessionState, error) { idToken, err := p.verifier.Verify(ctx, rawIDToken) if err != nil { return nil, fmt.Errorf("identity/google: could not verify id_token %v", err) } - var claims struct { Email string `json:"email"` EmailVerified bool `json:"email_verified"` @@ -167,39 +202,13 @@ func (p *GoogleProvider) Authenticate(code string) (*sessions.SessionState, erro return &sessions.SessionState{ IDToken: rawIDToken, - AccessToken: oauth2Token.AccessToken, - RefreshToken: oauth2Token.RefreshToken, - RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second), + RefreshDeadline: idToken.Expiry.Truncate(time.Second), Email: claims.Email, User: idToken.Subject, Groups: groups, }, nil } -// Refresh renews a user's session using an oid refresh token withoutreprompting the user. -// Group membership is also refreshed. -// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens -func (p *GoogleProvider) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) { - if s.RefreshToken == "" { - return nil, errors.New("identity: missing refresh token") - } - t := oauth2.Token{RefreshToken: s.RefreshToken} - newToken, err := p.oauth.TokenSource(ctx, &t).Token() - if err != nil { - log.Error().Err(err).Msg("identity: refresh failed") - return nil, err - } - s.AccessToken = newToken.AccessToken - s.RefreshDeadline = newToken.Expiry.Truncate(time.Second) - // validate groups - groups, err := p.UserGroups(ctx, s.User) - if err != nil { - return nil, fmt.Errorf("identity/google: could not retrieve groups %v", err) - } - s.Groups = groups - return s, nil -} - // UserGroups returns a slice of group names a given user is in // NOTE: groups via Directory API is limited to 1 QPS! // https://developers.google.com/admin-sdk/directory/v1/reference/groups/list diff --git a/internal/identity/microsoft.go b/internal/identity/microsoft.go index 0f2d6d29c..615e25981 100644 --- a/internal/identity/microsoft.go +++ b/internal/identity/microsoft.go @@ -74,8 +74,7 @@ func NewAzureProvider(p *Provider) (*AzureProvider, error) { // Authenticate creates an identity session with azure from a authorization code, and follows up // call to the groups api to check what groups the user is in. -func (p *AzureProvider) Authenticate(code string) (*sessions.SessionState, error) { - ctx := context.Background() +func (p *AzureProvider) Authenticate(ctx context.Context, code string) (*sessions.SessionState, error) { // convert authorization code into a token oauth2Token, err := p.oauth.Exchange(ctx, code) if err != nil { @@ -88,11 +87,23 @@ func (p *AzureProvider) Authenticate(code string) (*sessions.SessionState, error return nil, fmt.Errorf("identity/microsoft: response did not contain an id_token") } // Parse and verify ID Token payload. + session, err := p.IDTokenToSession(ctx, rawIDToken) + if err != nil { + return nil, fmt.Errorf("identity/microsoft: could not verify id_token %v", err) + } + session.AccessToken = oauth2Token.AccessToken + session.RefreshToken = oauth2Token.RefreshToken + return session, nil +} + +// IDTokenToSession takes an identity provider issued JWT as input ('id_token') +// and returns a session state. The provided token's audience ('aud') must +// match Pomerium's client_id. +func (p *AzureProvider) IDTokenToSession(ctx context.Context, rawIDToken string) (*sessions.SessionState, error) { idToken, err := p.verifier.Verify(ctx, rawIDToken) if err != nil { return nil, fmt.Errorf("identity/microsoft: could not verify id_token %v", err) } - var claims struct { Email string `json:"email"` EmailVerified bool `json:"email_verified"` @@ -101,8 +112,6 @@ func (p *AzureProvider) Authenticate(code string) (*sessions.SessionState, error if err := idToken.Claims(&claims); err != nil { return nil, fmt.Errorf("identity/microsoft: failed to parse id_token claims %v", err) } - - // google requires additional call to retrieve groups. groups, err := p.UserGroups(ctx, claims.Email) if err != nil { return nil, fmt.Errorf("identity/microsoft: could not retrieve groups %v", err) @@ -110,9 +119,7 @@ func (p *AzureProvider) Authenticate(code string) (*sessions.SessionState, error return &sessions.SessionState{ IDToken: rawIDToken, - AccessToken: oauth2Token.AccessToken, - RefreshToken: oauth2Token.RefreshToken, - RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second), + RefreshDeadline: idToken.Expiry.Truncate(time.Second), Email: claims.Email, User: idToken.Subject, Groups: groups, diff --git a/internal/identity/mock_provider.go b/internal/identity/mock_provider.go index 8e231cc71..cb173f7bd 100644 --- a/internal/identity/mock_provider.go +++ b/internal/identity/mock_provider.go @@ -8,21 +8,28 @@ import ( // MockProvider provides a mocked implementation of the providers interface. type MockProvider struct { - AuthenticateResponse sessions.SessionState - AuthenticateError error - ValidateResponse bool - ValidateError error - RefreshResponse *sessions.SessionState - RefreshError error - RevokeError error - GetSignInURLResponse string + AuthenticateResponse sessions.SessionState + AuthenticateError error + IDTokenToSessionResponse sessions.SessionState + IDTokenToSessionError error + ValidateResponse bool + ValidateError error + RefreshResponse *sessions.SessionState + RefreshError error + RevokeError error + GetSignInURLResponse string } // Authenticate is a mocked providers function. -func (mp MockProvider) Authenticate(code string) (*sessions.SessionState, error) { +func (mp MockProvider) Authenticate(ctx context.Context, code string) (*sessions.SessionState, error) { return &mp.AuthenticateResponse, mp.AuthenticateError } +// IDTokenToSession is a mocked providers function. +func (mp MockProvider) IDTokenToSession(ctx context.Context, code string) (*sessions.SessionState, error) { + return &mp.IDTokenToSessionResponse, mp.IDTokenToSessionError +} + // Validate is a mocked providers function. func (mp MockProvider) Validate(ctx context.Context, s string) (bool, error) { return mp.ValidateResponse, mp.ValidateError diff --git a/internal/identity/oidc.go b/internal/identity/oidc.go index 93b80800a..8624ae8ab 100644 --- a/internal/identity/oidc.go +++ b/internal/identity/oidc.go @@ -14,7 +14,7 @@ type OIDCProvider struct { *Provider } -// NewOIDCProvider creates a new instance of an OpenID Connect provider. +// NewOIDCProvider creates a new instance of a generic OpenID Connect provider. func NewOIDCProvider(p *Provider) (*OIDCProvider, error) { ctx := context.Background() if p.ProviderURL == "" { diff --git a/internal/identity/okta.go b/internal/identity/okta.go index 8ef0944b8..a101f5fd5 100644 --- a/internal/identity/okta.go +++ b/internal/identity/okta.go @@ -83,12 +83,6 @@ func (p *OktaProvider) Revoke(token string) error { return nil } -// GetSignInURL returns the sign in url with typical oauth parameters -// Google requires access type offline -func (p *OktaProvider) GetSignInURL(state string) string { - return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline) -} - type accessToken struct { Subject string `json:"sub"` Groups []string `json:"groups"` diff --git a/internal/identity/providers.go b/internal/identity/providers.go index ee8b7b677..f2d21a00f 100644 --- a/internal/identity/providers.go +++ b/internal/identity/providers.go @@ -1,5 +1,5 @@ -// Package identity provides support for making OpenID Connect and OAuth2 authorized and -// authenticated HTTP requests with third party identity providers. +// Package identity provides support for making OpenID Connect (OIDC) +// and OAuth2 authenticated HTTP requests with third party identity providers. package identity // import "github.com/pomerium/pomerium/internal/identity" import ( @@ -44,14 +44,15 @@ type UserGrouper interface { // Authenticator is an interface representing the ability to authenticate with an identity provider. type Authenticator interface { - Authenticate(string) (*sessions.SessionState, error) + Authenticate(context.Context, string) (*sessions.SessionState, error) + IDTokenToSession(context.Context, string) (*sessions.SessionState, error) Validate(context.Context, string) (bool, error) Refresh(context.Context, *sessions.SessionState) (*sessions.SessionState, error) Revoke(string) error GetSignInURL(state string) string } -// New returns a new identity provider based given its name. +// New returns a new identity provider based on its name. // Returns an error if selected provided not found or if the identity provider is not known. func New(providerName string, p *Provider) (a Authenticator, err error) { switch providerName { @@ -124,10 +125,37 @@ func (p *Provider) Validate(ctx context.Context, idToken string) (bool, error) { return true, nil } +// IDTokenToSession takes an identity provider issued JWT as input ('id_token') +// and returns a session state. The provided token's audience ('aud') must +// match Pomerium's client_id. +func (p *Provider) IDTokenToSession(ctx context.Context, rawIDToken string) (*sessions.SessionState, error) { + idToken, err := p.verifier.Verify(ctx, rawIDToken) + if err != nil { + return nil, fmt.Errorf("identity: could not verify id_token: %v", err) + } + // extract additional, non-oidc standard claims + var claims struct { + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + Groups []string `json:"groups"` + } + if err := idToken.Claims(&claims); err != nil { + return nil, fmt.Errorf("identity: failed to parse id_token claims: %v", err) + } + + return &sessions.SessionState{ + IDToken: rawIDToken, + User: idToken.Subject, + RefreshDeadline: idToken.Expiry.Truncate(time.Second), + Email: claims.Email, + Groups: claims.Groups, + }, nil + +} + // Authenticate creates a session with an identity provider from a authorization code -func (p *Provider) Authenticate(code string) (*sessions.SessionState, error) { - ctx := context.Background() - // convert authorization code into a token +func (p *Provider) Authenticate(ctx context.Context, code string) (*sessions.SessionState, error) { + // exchange authorization for a oidc token oauth2Token, err := p.oauth.Exchange(ctx, code) if err != nil { return nil, fmt.Errorf("identity: failed token exchange: %v", err) @@ -137,36 +165,18 @@ func (p *Provider) Authenticate(code string) (*sessions.SessionState, error) { if !ok { return nil, fmt.Errorf("token response did not contain an id_token") } - // Parse and verify ID Token payload. - idToken, err := p.verifier.Verify(ctx, rawIDToken) + session, err := p.IDTokenToSession(ctx, rawIDToken) if err != nil { return nil, fmt.Errorf("identity: could not verify id_token: %v", err) } + session.AccessToken = oauth2Token.AccessToken + session.RefreshToken = oauth2Token.RefreshToken - // Extract id_token which contains claims about the authenticated user - var claims struct { - Email string `json:"email"` - EmailVerified bool `json:"email_verified"` - Groups []string `json:"groups"` - } - // parse claims from the raw, encoded jwt token - if err := idToken.Claims(&claims); err != nil { - return nil, fmt.Errorf("identity: failed to parse id_token claims: %v", err) - } - - return &sessions.SessionState{ - IDToken: rawIDToken, - AccessToken: oauth2Token.AccessToken, - RefreshToken: oauth2Token.RefreshToken, - RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second), - Email: claims.Email, - User: idToken.Subject, - Groups: claims.Groups, - }, nil + return session, nil } -// Refresh renews a user's session using an oid refresh token without reprompting the user. -// Group membership is also refreshed. +// Refresh renews a user's session using therefresh_token without reprompting +// the user. If supported, group membership is also refreshed. // https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens func (p *Provider) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) { if s.RefreshToken == "" { diff --git a/internal/sessions/rest_store.go b/internal/sessions/rest_store.go new file mode 100644 index 000000000..8bd3effcd --- /dev/null +++ b/internal/sessions/rest_store.go @@ -0,0 +1,107 @@ +package sessions // import "github.com/pomerium/pomerium/internal/sessions" + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "github.com/pomerium/pomerium/internal/cryptutil" +) + +// DefaultBearerTokenHeader is default header name for the authorization bearer +// token header as defined in rfc2617 +// https://tools.ietf.org/html/rfc6750#section-2.1 +const DefaultBearerTokenHeader = "Authorization" + +// RestStore is a session store suitable for REST +type RestStore struct { + Name string + Cipher cryptutil.Cipher + // Expire time.Duration +} + +// RestStoreOptions contains the options required to build a new RestStore. +type RestStoreOptions struct { + Name string + Cipher cryptutil.Cipher + // Expire time.Duration +} + +// NewRestStore creates a new RestStore from a set of RestStoreOptions. +func NewRestStore(opts *RestStoreOptions) (*RestStore, error) { + if opts.Name == "" { + opts.Name = DefaultBearerTokenHeader + } + if opts.Cipher == nil { + return nil, fmt.Errorf("internal/sessions: cipher cannot be nil") + } + return &RestStore{ + Name: opts.Name, + // Expire: opts.Expire, + Cipher: opts.Cipher, + }, nil +} + +// ClearSession functions differently because REST is stateless, we instead +// inform the client that this token is no longer valid. +// https://tools.ietf.org/html/rfc6750 +func (s *RestStore) ClearSession(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + errMsg := ` + { + "error": "invalid_token", + "token_type": "Bearer", + "error_description": "The token has expired." + }` + w.Write([]byte(errMsg)) + return +} + +// LoadSession attempts to load a pomerium session from a Bearer Token set +// in the authorization header. +func (s *RestStore) LoadSession(r *http.Request) (*SessionState, error) { + authHeader := r.Header.Get(s.Name) + split := strings.Split(authHeader, "Bearer") + if authHeader == "" || len(split) != 2 { + return nil, errors.New("internal/sessions: no bearer token header found") + } + token := strings.TrimSpace(split[1]) + session, err := UnmarshalSession(token, s.Cipher) + if err != nil { + return nil, err + } + return session, nil +} + +// RestStoreResponse is the JSON struct returned to the client. +type RestStoreResponse struct { + // Token is the encrypted pomerium session that can be used to + // programmatically authenticate with pomerium. + Token string + // In addition to the token, non-sensitive meta data is returned to help + // the client manage token renewals. + Expiry time.Time +} + +// SaveSession returns an encrypted pomerium session as a JSON object with +// associated, non sensitive meta-data like +func (s *RestStore) SaveSession(w http.ResponseWriter, r *http.Request, sessionState *SessionState) error { + encToken, err := MarshalSession(sessionState, s.Cipher) + if err != nil { + return err + } + jsonBytes, err := json.Marshal( + &RestStoreResponse{ + Token: encToken, + Expiry: sessionState.RefreshDeadline, + }) + if err != nil { + return fmt.Errorf("internal/sessions: couldn't marshal token struct: %v", err) + } + w.Header().Set("Content-Type", "application/json") + w.Write(jsonBytes) + return nil +} diff --git a/internal/sessions/rest_store_test.go b/internal/sessions/rest_store_test.go new file mode 100644 index 000000000..cfddedd32 --- /dev/null +++ b/internal/sessions/rest_store_test.go @@ -0,0 +1,135 @@ +package sessions + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/pomerium/pomerium/internal/cryptutil" +) + +func TestRestStore_SaveSession(t *testing.T) { + now := time.Date(2008, 1, 8, 17, 5, 05, 0, time.UTC) + + tests := []struct { + name string + optionsName string + optionsCipher cryptutil.Cipher + sessionState *SessionState + wantErr bool + wantSaveResponse string + }{ + {"good", "Authenticate", &cryptutil.MockCipher{MarshalResponse: "test"}, &SessionState{RefreshDeadline: now}, false, `{"Token":"test","Expiry":"2008-01-08T17:05:05Z"}`}, + {"bad session marshal", "Authenticate", &cryptutil.MockCipher{MarshalError: errors.New("error")}, &SessionState{RefreshDeadline: now}, true, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, err := NewRestStore( + &RestStoreOptions{ + Name: tt.optionsName, + Cipher: tt.optionsCipher, + }) + if err != nil { + t.Fatalf("NewRestStore err %v", err) + } + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + if err := s.SaveSession(w, r, tt.sessionState); (err != nil) != tt.wantErr { + t.Errorf("RestStore.SaveSession() error = %v, wantErr %v", err, tt.wantErr) + } + resp := w.Result() + body, _ := ioutil.ReadAll(resp.Body) + if diff := cmp.Diff(string(body), tt.wantSaveResponse); diff != "" { + t.Errorf("RestStore.SaveSession() got / want diff \n%s\n", diff) + } + }) + } +} + +func TestNewRestStore(t *testing.T) { + + tests := []struct { + name string + optionsName string + optionsCipher cryptutil.Cipher + wantErr bool + }{ + {"good", "Authenticate", &cryptutil.MockCipher{}, false}, + {"good default to authenticate", "", &cryptutil.MockCipher{}, false}, + {"empty cipher", "Authenticate", nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewRestStore( + &RestStoreOptions{ + Name: tt.optionsName, + Cipher: tt.optionsCipher, + }) + if (err != nil) != tt.wantErr { + t.Errorf("NewRestStore() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func TestRestStore_ClearSession(t *testing.T) { + tests := []struct { + name string + expectedStatus int + }{ + {"always returns reset!", http.StatusUnauthorized}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &RestStore{Name: "Authenticate", Cipher: &cryptutil.MockCipher{}} + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + s.ClearSession(w, r) + resp := w.Result() + if diff := cmp.Diff(resp.StatusCode, tt.expectedStatus); diff != "" { + t.Errorf("RestStore.ClearSession() got / want diff \n%s\n", diff) + } + + }) + } +} + +func TestRestStore_LoadSession(t *testing.T) { + + tests := []struct { + name string + optionsName string + optionsCipher cryptutil.Cipher + token string + wantErr bool + }{ + {"good", "Authorization", &cryptutil.MockCipher{}, "test", false}, + {"empty auth header", "", &cryptutil.MockCipher{}, "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &RestStore{ + Name: tt.optionsName, + Cipher: tt.optionsCipher, + } + + r := httptest.NewRequest(http.MethodGet, "/", nil) + + if tt.optionsName != "" { + r.Header.Set(tt.optionsName, fmt.Sprintf(("Bearer %s"), tt.token)) + + } + _, err := s.LoadSession(r) + if (err != nil) != tt.wantErr { + t.Errorf("RestStore.LoadSession() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/proxy/handlers.go b/proxy/handlers.go index 9b13222e5..03d703410 100644 --- a/proxy/handlers.go +++ b/proxy/handlers.go @@ -25,7 +25,7 @@ type StateParameter struct { RedirectURI string `json:"redirect_uri"` } -// Handler returns a http handler for a Proxy +// Handler returns the proxy service's ServeMux func (p *Proxy) Handler() http.Handler { // validation middleware chain validate := middleware.NewChain() @@ -62,13 +62,11 @@ func (p *Proxy) SignOutCallback(w http.ResponseWriter, r *http.Request) { // OAuthStart begins the authenticate flow, encrypting the redirect url // in a request to the provider's sign in endpoint. func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) { - requestURI := r.URL.String() - callbackURL := p.GetRedirectURL(r.Host) - // CSRF value used to mitigate replay attacks. + // create a CSRF value used to mitigate replay attacks. state := &StateParameter{ SessionID: fmt.Sprintf("%x", cryptutil.GenerateKey()), - RedirectURI: requestURI, + RedirectURI: r.URL.String(), } // Encrypt, and save CSRF state. Will be checked on callback. @@ -104,7 +102,7 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) { return } - signinURL := p.GetSignInURL(p.AuthenticateURL, callbackURL, remoteState) + signinURL := p.GetSignInURL(p.AuthenticateURL, p.GetRedirectURL(r.Host), remoteState) log.FromRequest(r).Debug().Str("SigninURL", signinURL.String()).Msg("proxy: oauth start") // Redirect the user to the authenticate service along with the encrypted @@ -185,14 +183,14 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) { // shouldSkipAuthentication contains conditions for skipping authentication. // Conditions should be few in number and have strong justifications. func (p *Proxy) shouldSkipAuthentication(r *http.Request) bool { - pol, foundPolicy := p.policy(r) + policy, policyExists := p.policy(r) - if isCORSPreflight(r) && foundPolicy && pol.CORSAllowPreflight { + if isCORSPreflight(r) && policyExists && policy.CORSAllowPreflight { log.FromRequest(r).Debug().Msg("proxy: skipping authentication for valid CORS preflight request") return true } - if foundPolicy && pol.AllowPublicUnauthenticatedAccess { + if policyExists && policy.AllowPublicUnauthenticatedAccess { log.FromRequest(r).Debug().Msg("proxy: skipping authentication for public route") return true } @@ -214,19 +212,26 @@ func isCORSPreflight(r *http.Request) bool { // or starting the authenticate service for validation if not. func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) { if !p.shouldSkipAuthentication(r) { - s, err := p.sessionStore.LoadSession(r) + s, err := p.restStore.LoadSession(r) if err != nil { - switch err { - case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession: - log.FromRequest(r).Debug().Err(err).Msg("proxy: invalid session") - p.sessionStore.ClearSession(w, r) - p.OAuthStart(w, r) - return - default: - log.FromRequest(r).Error().Err(err).Msg("proxy: unexpected error") - httpErr := &httputil.Error{Message: "An unexpected error occurred", Code: http.StatusInternalServerError} - httputil.ErrorResponse(w, r, httpErr) - return + log.FromRequest(r).Debug().Err(err).Msg("proxy: no bearer auth token found") + } + + if s == nil { + s, err = p.sessionStore.LoadSession(r) + if err != nil { + switch err { + case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession: + log.FromRequest(r).Debug().Err(err).Msg("proxy: invalid session") + p.sessionStore.ClearSession(w, r) + p.OAuthStart(w, r) + return + default: + log.FromRequest(r).Error().Err(err).Msg("proxy: unexpected error") + httpErr := &httputil.Error{Message: "An unexpected error occurred", Code: http.StatusInternalServerError} + httputil.ErrorResponse(w, r, httpErr) + return + } } } diff --git a/proxy/handlers_test.go b/proxy/handlers_test.go index d6e826edf..3e27eb9f2 100644 --- a/proxy/handlers_test.go +++ b/proxy/handlers_test.go @@ -510,3 +510,50 @@ func TestProxy_Impersonate(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.StatusBadRequest}, + {"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}, + {"malformed", 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.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() + if tt.name == "malformed" { + req.URL.RawQuery = "email=%zzzzz" + } + 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) + } + }) + } + +} diff --git a/proxy/proxy.go b/proxy/proxy.go index e16f5d7f1..788c3500d 100755 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -13,7 +13,6 @@ import ( "time" "github.com/pomerium/pomerium/internal/config" - "github.com/pomerium/pomerium/internal/cryptutil" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/policy" @@ -38,10 +37,10 @@ const ( func ValidateOptions(o config.Options) error { decoded, err := base64.StdEncoding.DecodeString(o.SharedKey) if err != nil { - return fmt.Errorf("authorize: `SHARED_SECRET` setting is invalid base64: %v", err) + return fmt.Errorf("`SHARED_SECRET` setting is invalid base64: %v", err) } if len(decoded) != 32 { - return fmt.Errorf("authorize: `SHARED_SECRET` want 32 but got %d bytes", len(decoded)) + return fmt.Errorf("`SHARED_SECRET` want 32 but got %d bytes", len(decoded)) } if len(o.Policies) == 0 { return errors.New("missing setting: no policies defined") @@ -92,6 +91,7 @@ type Proxy struct { cipher cryptutil.Cipher csrfStore sessions.CSRFStore sessionStore sessions.SessionStore + restStore sessions.SessionStore redirectURL *url.URL templates *template.Template @@ -130,7 +130,10 @@ func New(opts config.Options) (*Proxy, error) { if err != nil { return nil, err } - + restStore, err := sessions.NewRestStore(&sessions.RestStoreOptions{Cipher: cipher}) + if err != nil { + return nil, err + } p := &Proxy{ routeConfigs: make(map[string]*routeConfig), // services @@ -139,6 +142,7 @@ func New(opts config.Options) (*Proxy, error) { cipher: cipher, csrfStore: cookieStore, sessionStore: cookieStore, + restStore: restStore, SharedKey: opts.SharedKey, redirectURL: &url.URL{Path: "/.pomerium/callback"}, templates: templates.New(), @@ -283,7 +287,7 @@ func urlParse(uri string) (*url.URL, error) { return url.ParseRequestURI(uri) } -// UpdateOptions updates internal structres based on config.Options +// UpdateOptions updates internal structures based on config.Options func (p *Proxy) UpdateOptions(o config.Options) error { log.Info().Msg("proxy: updating options") err := p.UpdatePolicies(o) diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index de2b63f5c..e565eba1f 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -1,7 +1,6 @@ package proxy // import "github.com/pomerium/pomerium/proxy" import ( - "errors" "io/ioutil" "net" "net/http" @@ -11,8 +10,6 @@ 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" ) @@ -173,6 +170,8 @@ func TestOptions_Validate(t *testing.T) { invalidSignKey.SigningKey = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw^" badSharedKey := testOptions() badSharedKey.SharedKey = "" + sharedKeyBadBas64 := testOptions() + sharedKeyBadBas64.SharedKey = "%(*@389" missingPolicy := testOptions() missingPolicy.Policies = []policy.Policy{} @@ -193,6 +192,7 @@ func TestOptions_Validate(t *testing.T) { {"no shared secret", badSharedKey, true}, {"invalid signing key", invalidSignKey, true}, {"missing policy", missingPolicy, true}, + {"shared secret bad base64", sharedKeyBadBas64, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -240,49 +240,6 @@ 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.StatusBadRequest}, - {"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) - } - }) - } - -} - func Test_UpdateOptions(t *testing.T) { good := testOptions() diff --git a/scripts/programmatic_access.py b/scripts/programmatic_access.py new file mode 100755 index 000000000..e3033b8e9 --- /dev/null +++ b/scripts/programmatic_access.py @@ -0,0 +1,79 @@ +from __future__ import absolute_import, division, print_function + +import argparse +import json +import sys + +import requests + +parser = argparse.ArgumentParser() +parser.add_argument('--openid-configuration', + default="https://accounts.google.com/.well-known/openid-configuration") +parser.add_argument('--client-id') +parser.add_argument('--client-secret') +parser.add_argument('--pomerium-client-id') +parser.add_argument('--code') +parser.add_argument('--pomerium-token-url', + default="https://authenticate.corp.beyondperimeter.com/api/v1/token") +parser.add_argument('--pomerium-token') +parser.add_argument('--pomerium-url', default="https://httpbin.corp.beyondperimeter.com/get") + + +def main(): + args = parser.parse_args() + code = args.code + pomerium_token = args.pomerium_token + oidc_document = requests.get(args.openid_configuration).json() + token_url = oidc_document['token_endpoint'] + print(token_url) + sign_in_url = oidc_document['authorization_endpoint'] + + if not code and not pomerium_token: + if not args.client_id: + print("client-id is required") + sys.exit(1) + + sign_in_url = "{}?response_type=code&scope=openid%20email&access_type=offline&redirect_uri=urn:ietf:wg:oauth:2.0:oob&client_id={}".format( + oidc_document['authorization_endpoint'], args.client_id) + print("Access code not set, so we'll do the process interactively!") + print("Go to the url : {}".format(sign_in_url)) + code = input("Complete the login and enter your code:") + print(code) + + if not pomerium_token: + req = requests.post( + token_url, { + 'client_id': args.client_id, + 'client_secret': args.client_secret, + 'code': code, + 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', + 'grant_type': 'authorization_code' + }) + + refresh_token = req.json()['refresh_token'] + print("refresh token: {}".format(refresh_token)) + + print("create a new id_token with our pomerium app as the audience") + req = requests.post( + token_url, { + 'refresh_token': refresh_token, + 'client_id': args.client_id, + 'client_secret': args.client_secret, + 'audience': args.pomerium_client_id, + 'grant_type': 'refresh_token' + }) + id_token = req.json()['id_token'] + print("pomerium id_token: {}".format(id_token)) + + print("exchange our identity providers id token for a pomerium bearer token") + req = requests.post(args.pomerium_token_url, {'id_token': id_token}) + pomerium_token = req.json()['Token'] + print("pomerium bearer token is: {}".format(pomerium_token)) + + req = requests.get(args.pomerium_url, headers={'Authorization': 'Bearer ' + pomerium_token}) + json_formatted = json.dumps(req.json(), indent=1) + print(json_formatted) + + +if __name__ == '__main__': + main() diff --git a/scripts/programmatic_access.sh b/scripts/programmatic_access.sh new file mode 100755 index 000000000..9b513de52 --- /dev/null +++ b/scripts/programmatic_access.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# Create a new OAUTH2 provider DISTINCT from your pomerium configuration +# Select type as "OTHER" +CLIENT_ID='REPLACE-ME.apps.googleusercontent.com' +CLIENT_SECRET='REPLACE-ME' +SIGNIN_URL='https://accounts.google.com/o/oauth2/v2/auth?client_id='$CLIENT_ID'&response_type=code&scope=openid%20email&access_type=offline&redirect_uri=urn:ietf:wg:oauth:2.0:oob' + +# This would be your pomerium client id +POMERIUM_CLIENT_ID='REPLACE-ME.apps.googleusercontent.com' + +echo "Follow the following URL to get an offline auth code from your IdP" +echo $SIGNIN_URL + +read -p 'Enter the authorization code as a result of logging in: ' CODE +echo $CODE + +echo "Exchange our authorization code to get a refresh_token" +echo "refresh_tokens can be used to generate indefinite access tokens / id_tokens" +curl \ + -d client_id=$CLIENT_ID \ + -d client_secret=$CLIENT_SECRET \ + -d code=$CODE \ + -d redirect_uri=urn:ietf:wg:oauth:2.0:oob \ + -d grant_type=authorization_code \ + https://www.googleapis.com/oauth2/v4/token + +read -p 'Enter the refresh token result:' REFRESH_TOKEN +echo $REFRESH_TOKEN + +echo "Use our refresh_token to create a new id_token with an audience of pomerium's oauth client" +curl \ + -d client_id=$CLIENT_ID \ + -d client_secret=$CLIENT_SECRET \ + -d refresh_token=$REFRESH_TOKEN \ + -d grant_type=refresh_token \ + -d audience=$POMERIUM_CLIENT_ID \ + https://www.googleapis.com/oauth2/v4/token + +echo "now we have an id_token with an audience that matches that of our pomerium app" +read -p 'Enter the resulting id_token:' ID_TOKEN +echo $ID_TOKEN + +curl -X POST \ + -d id_token=$ID_TOKEN \ + https://authenticate.corp.beyondperimeter.com/api/v1/token + +read -p 'Enter the resulting Token:' POMERIUM_ACCESS_TOKEN +echo $POMERIUM_ACCESS_TOKEN + +echo "we have our bearer token that can be used with pomerium now" +curl \ + -H "Authorization: Bearer ${POMERIUM_ACCESS_TOKEN}" \ + "https://httpbin.corp.beyondperimeter.com/"