authenticate: programmatic access support

- authenticate: added a token exchange api endpoint that converts
  an identity provider's JWT into a pomerium session.
- internal/identity: authenticate now passes context.
- internal/identity: removed extraneous GetSignInURL from okta.
- internal/sessions: add rest store
- update go.mod / go.sum depedencies.
- docs: add programmatic examples in shell and python
This commit is contained in:
Bobby DeSimone 2019-06-12 14:51:19 -07:00
parent 2025c54899
commit cf0f98536a
No known key found for this signature in database
GPG key ID: AEE4CF12FE86D07E
22 changed files with 910 additions and 256 deletions

View file

@ -1,5 +1,15 @@
# Pomerium Changelog
## vUNRELEASED
### NEW
- Add programmatic authentication support. [GH-177]
### CHANGED
### FIXED
## v0.0.5
### NEW

View file

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

View file

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

View file

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

View file

@ -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"]
}
];
}

View file

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

24
go.mod
View file

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

180
go.sum
View file

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

View file

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

View file

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

View file

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

View file

@ -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 == "" {

View file

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

View file

@ -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 == "" {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

79
scripts/programmatic_access.py Executable file
View file

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

53
scripts/programmatic_access.sh Executable file
View file

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