authenticate: use gRPC for service endpoints (#39)

* authenticate: set cookie secure as default.
* authenticate: remove single flight provider.
* authenticate/providers: Rename “ProviderData” to “IdentityProvider”
* authenticate/providers: Fixed an issue where scopes were not being overwritten
* proxy/authenticate : http client code removed.
* proxy: standardized session variable names between services.
* docs: change basic docker-config to be an “all-in-one” example with no nginx load.
* docs:  nginx balanced docker compose example with intra-ingress settings.
* license:  attribution for adaptation of goji’s middleware pattern.
This commit is contained in:
Bobby DeSimone 2019-02-08 10:10:38 -08:00 committed by GitHub
parent 9ca3ff4fa2
commit c886b924e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 2184 additions and 1463 deletions

View file

@ -105,6 +105,30 @@ https://github.com/justinas/alice/blob/master/LICENSE
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
goji
SPDX-License-Identifier: MIT
https://github.com/zenazn/goji/blob/master/LICENSE
Copyright (c) 2014, 2015, 2016 Carl Jackson (carl@avtok.com)
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR

View file

@ -18,18 +18,19 @@ import (
)
var defaultOptions = &Options{
CookieName: "_pomerium_authenticate",
CookieHTTPOnly: true,
CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(1) * time.Hour,
SessionLifetimeTTL: time.Duration(720) * time.Hour,
Scopes: []string{"openid", "email", "profile"},
CookieName: "_pomerium_authenticate",
CookieHTTPOnly: true,
CookieSecure: true,
CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(30) * time.Minute,
CookieLifetimeTTL: time.Duration(720) * time.Hour,
}
// Options permits the configuration of the authentication service
// Options details the available configuration settings for the authenticate service
type Options struct {
RedirectURL *url.URL `envconfig:"REDIRECT_URL"`
// SharedKey is used to authenticate requests between services
SharedKey string `envconfig:"SHARED_SECRET"`
// Coarse authorization based on user email domain
@ -37,27 +38,25 @@ type Options struct {
ProxyRootDomains []string `envconfig:"PROXY_ROOT_DOMAIN"`
// Session/Cookie management
CookieName string
CookieSecret string `envconfig:"COOKIE_SECRET"`
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
CookieSecure bool `envconfig:"COOKIE_SECURE"`
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
CookieName string
CookieSecret string `envconfig:"COOKIE_SECRET"`
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
CookieSecure bool `envconfig:"COOKIE_SECURE"`
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
CookieLifetimeTTL time.Duration `envconfig:"COOKIE_LIFETIME"`
SessionLifetimeTTL time.Duration `envconfig:"SESSION_LIFETIME_TTL"`
// Authentication provider configuration variables as specified by RFC6749
// IdentityProvider provider configuration variables as specified by RFC6749
// See: https://openid.net/specs/openid-connect-basic-1_0.html#RFC6749
ClientID string `envconfig:"IDP_CLIENT_ID"`
ClientSecret string `envconfig:"IDP_CLIENT_SECRET"`
Provider string `envconfig:"IDP_PROVIDER"`
ProviderURL string `envconfig:"IDP_PROVIDER_URL"`
Scopes []string `envconfig:"IDP_SCOPE"`
Scopes []string `envconfig:"IDP_SCOPES"`
}
// OptionsFromEnvConfig builds the authentication service's configuration
// options from provided environmental variables
// OptionsFromEnvConfig builds the authenticate service's configuration environmental variables
func OptionsFromEnvConfig() (*Options, error) {
o := defaultOptions
if err := envconfig.Process("", o); err != nil {
@ -66,7 +65,7 @@ func OptionsFromEnvConfig() (*Options, error) {
return o, nil
}
// Validate checks to see if configuration values are valid for the authentication service.
// Validate checks to see if configuration values are valid for the authenticate service.
// The checks do not modify the internal state of the Option structure. Returns
// on first error found.
func (o *Options) Validate() error {
@ -102,8 +101,7 @@ func (o *Options) Validate() error {
return nil
}
// Authenticate is service for validating user authentication for proxied-requests
// against third-party identity provider (IdP) services.
// Authenticate validates a user's identity
type Authenticate struct {
RedirectURL *url.URL
@ -115,7 +113,7 @@ type Authenticate struct {
SharedKey string
SessionLifetimeTTL time.Duration
CookieLifetimeTTL time.Duration
templates *template.Template
csrfStore sessions.CSRFStore
@ -125,7 +123,7 @@ type Authenticate struct {
provider providers.Provider
}
// New validates and creates a new authentication service from a configuration options.
// New validates and creates a new authenticate service from a set of Options
func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate, error) {
if opts == nil {
return nil, errors.New("options cannot be nil")
@ -133,15 +131,9 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
if err := opts.Validate(); err != nil {
return nil, err
}
decodedAuthCodeSecret, err := base64.StdEncoding.DecodeString(opts.CookieSecret)
if err != nil {
return nil, err
}
cipher, err := cryptutil.NewCipher([]byte(decodedAuthCodeSecret))
if err != nil {
return nil, err
}
decodedCookieSecret, err := base64.StdEncoding.DecodeString(opts.CookieSecret)
// checked by validate
decodedCookieSecret, _ := base64.StdEncoding.DecodeString(opts.CookieSecret)
cipher, err := cryptutil.NewCipher([]byte(decodedCookieSecret))
if err != nil {
return nil, err
}
@ -183,25 +175,22 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
return nil, err
}
}
return p, nil
}
func newProvider(opts *Options) (providers.Provider, error) {
pd := &providers.ProviderData{
pd := &providers.IdentityProvider{
RedirectURL: opts.RedirectURL,
ProviderName: opts.Provider,
ProviderURL: opts.ProviderURL,
ClientID: opts.ClientID,
ClientSecret: opts.ClientSecret,
SessionLifetimeTTL: opts.SessionLifetimeTTL,
SessionLifetimeTTL: opts.CookieLifetimeTTL,
Scopes: opts.Scopes,
}
np, err := providers.New(opts.Provider, pd)
if err != nil {
return nil, err
}
return providers.NewSingleFlightProvider(np), nil
return np, err
}
func dotPrependDomains(d []string) []string {

View file

@ -8,23 +8,19 @@ import (
"time"
)
func init() {
os.Clearenv()
}
func testOptions() *Options {
redirectURL, _ := url.Parse("https://example.com/oauth2/callback")
return &Options{
ProxyRootDomains: []string{"example.com"},
AllowedDomains: []string{"example.com"},
RedirectURL: redirectURL,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
ClientID: "test-client-id",
ClientSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieRefresh: time.Duration(1) * time.Hour,
SessionLifetimeTTL: time.Duration(720) * time.Hour,
CookieExpire: time.Duration(168) * time.Hour,
ProxyRootDomains: []string{"example.com"},
AllowedDomains: []string{"example.com"},
RedirectURL: redirectURL,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
ClientID: "test-client-id",
ClientSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieRefresh: time.Duration(1) * time.Hour,
CookieLifetimeTTL: time.Duration(720) * time.Hour,
CookieExpire: time.Duration(168) * time.Hour,
}
}
@ -81,6 +77,8 @@ func TestOptions_Validate(t *testing.T) {
}
func TestOptionsFromEnvConfig(t *testing.T) {
os.Clearenv()
tests := []struct {
name string
want *Options
@ -91,7 +89,7 @@ func TestOptionsFromEnvConfig(t *testing.T) {
{"good default, no env settings", defaultOptions, "", "", false},
{"bad url", nil, "REDIRECT_URL", "%.rjlw", true},
{"good duration", defaultOptions, "COOKIE_EXPIRE", "1m", false},
{"bad duration", nil, "COOKIE_EXPIRE", "1sm", true},
{"bad duration", nil, "COOKIE_REFRESH", "1sm", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -131,3 +129,65 @@ func Test_dotPrependDomains(t *testing.T) {
})
}
}
func Test_newProvider(t *testing.T) {
redirectURL, _ := url.Parse("https://example.com/oauth3/callback")
goodOpts := &Options{
RedirectURL: redirectURL,
Provider: "google",
ProviderURL: "",
ClientID: "cllient-id",
ClientSecret: "client-secret",
}
tests := []struct {
name string
opts *Options
wantErr bool
}{
{"good", goodOpts, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := newProvider(tt.opts)
if (err != nil) != tt.wantErr {
t.Errorf("newProvider() error = %v, wantErr %v", err, tt.wantErr)
return
}
// if !reflect.DeepEqual(got, tt.want) {
// t.Errorf("newProvider() = %v, want %v", got, tt.want)
// }
})
}
}
func TestNew(t *testing.T) {
good := testOptions()
good.Provider = "google"
badRedirectURL := testOptions()
badRedirectURL.RedirectURL = nil
tests := []struct {
name string
opts *Options
// want *Authenticate
wantErr bool
}{
{"good", good, false},
{"empty opts", nil, true},
{"fails to validate", badRedirectURL, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := New(tt.opts)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return
}
// if !reflect.DeepEqual(got, tt.want) {
// t.Errorf("New() = %v, want %v", got, tt.want)
// }
})
}
}

64
authenticate/grpc.go Normal file
View file

@ -0,0 +1,64 @@
package authenticate // import "github.com/pomerium/pomerium/authenticate"
import (
"context"
"fmt"
"github.com/golang/protobuf/ptypes"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
pb "github.com/pomerium/pomerium/proto/authenticate"
)
// Authenticate takes an encrypted code, and returns the authentication result.
func (p *Authenticate) Authenticate(ctx context.Context, in *pb.AuthenticateRequest) (*pb.AuthenticateReply, error) {
session, err := sessions.UnmarshalSession(in.Code, p.cipher)
if err != nil {
return nil, fmt.Errorf("authenticate/grpc: %v", err)
}
expiryTimestamp, err := ptypes.TimestampProto(session.RefreshDeadline)
if err != nil {
return nil, err
}
return &pb.AuthenticateReply{
AccessToken: session.AccessToken,
RefreshToken: session.RefreshToken,
IdToken: session.IDToken,
User: session.User,
Email: session.Email,
Expiry: expiryTimestamp,
}, nil
}
// Validate locally validates a JWT id token; does NOT do nonce or revokation validation.
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func (p *Authenticate) Validate(ctx context.Context, in *pb.ValidateRequest) (*pb.ValidateReply, error) {
isValid, err := p.provider.Validate(in.IdToken)
if err != nil {
return &pb.ValidateReply{IsValid: false}, err
}
return &pb.ValidateReply{IsValid: isValid}, nil
}
// Refresh renews a user's session checks if the session has been revoked using an access token
// without reprompting the user.
func (p *Authenticate) Refresh(ctx context.Context, in *pb.RefreshRequest) (*pb.RefreshReply, error) {
newToken, err := p.provider.Refresh(in.RefreshToken)
if err != nil {
return nil, err
}
expiryTimestamp, err := ptypes.TimestampProto(newToken.Expiry)
if err != nil {
return nil, err
}
log.Info().
Str("session.AccessToken", newToken.AccessToken).
Msg("authenticate: grpc: refresh: ok")
return &pb.RefreshReply{
AccessToken: newToken.AccessToken,
Expiry: expiryTimestamp,
}, nil
}

170
authenticate/grpc_test.go Normal file
View file

@ -0,0 +1,170 @@
package authenticate
import (
"context"
"errors"
"reflect"
"testing"
"time"
"github.com/golang/protobuf/ptypes"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/sessions"
pb "github.com/pomerium/pomerium/proto/authenticate"
"golang.org/x/oauth2"
)
var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
// TestProvider is a mock provider
type testProvider struct{}
func (tp *testProvider) Authenticate(s string) (*sessions.SessionState, error) {
return &sessions.SessionState{}, nil
}
func (tp *testProvider) Revoke(s string) error { return nil }
func (tp *testProvider) GetSignInURL(s string) string { return "/signin" }
func (tp *testProvider) Refresh(s string) (*oauth2.Token, error) {
if s == "error" {
return nil, errors.New("failed refresh")
}
if s == "bad time" {
return &oauth2.Token{AccessToken: "updated", Expiry: time.Time{}}, nil
}
return &oauth2.Token{AccessToken: "updated", Expiry: fixedDate}, nil
}
func (tp *testProvider) Validate(token string) (bool, error) {
if token == "good" {
return true, nil
} else if token == "error" {
return false, errors.New("error validating id token")
}
return false, nil
}
func TestAuthenticate_Validate(t *testing.T) {
tests := []struct {
name string
idToken string
want bool
wantErr bool
}{
{"good", "example", false, false},
{"error", "error", false, true},
{"not error", "not error", false, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tp := &testProvider{}
p := &Authenticate{provider: tp}
got, err := p.Validate(context.Background(), &pb.ValidateRequest{IdToken: tt.idToken})
if (err != nil) != tt.wantErr {
t.Errorf("Authenticate.Validate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got.IsValid, tt.want) {
t.Errorf("Authenticate.Validate() = %v, want %v", got.IsValid, tt.want)
}
})
}
}
func TestAuthenticate_Refresh(t *testing.T) {
fixedProtoTime, err := ptypes.TimestampProto(fixedDate)
if err != nil {
t.Fatal("failed to parse timestamp")
}
tests := []struct {
name string
refreshToken string
want *pb.RefreshReply
wantErr bool
}{
{"good", "refresh-token", &pb.RefreshReply{AccessToken: "updated", Expiry: fixedProtoTime}, false},
{"test error", "error", nil, true},
// {"test bad time", "bad time", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tp := &testProvider{}
p := &Authenticate{provider: tp}
got, err := p.Refresh(context.Background(), &pb.RefreshRequest{RefreshToken: tt.refreshToken})
if (err != nil) != tt.wantErr {
t.Errorf("Authenticate.Refresh() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Authenticate.Refresh() = %v, want %v", got, tt.want)
}
})
}
}
func TestAuthenticate_Authenticate(t *testing.T) {
secret := cryptutil.GenerateKey()
c, err := cryptutil.NewCipher([]byte(secret))
if err != nil {
t.Fatalf("expected to be able to create cipher: %v", err)
}
newSecret := cryptutil.GenerateKey()
c2, err := cryptutil.NewCipher([]byte(newSecret))
if err != nil {
t.Fatalf("expected to be able to create cipher: %v", err)
}
lt := time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC()
rt := time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC()
vt := time.Now().Add(1 * time.Minute).Truncate(time.Second).UTC()
vtProto, err := ptypes.TimestampProto(rt)
if err != nil {
t.Fatal("failed to parse timestamp")
}
want := &sessions.SessionState{
AccessToken: "token1234",
RefreshToken: "refresh4321",
LifetimeDeadline: lt,
RefreshDeadline: rt,
ValidDeadline: vt,
Email: "user@domain.com",
User: "user",
}
goodReply := &pb.AuthenticateReply{
AccessToken: "token1234",
RefreshToken: "refresh4321",
Expiry: vtProto,
Email: "user@domain.com",
User: "user"}
ciphertext, err := sessions.MarshalSession(want, c)
if err != nil {
t.Fatalf("expected to be encode session: %v", err)
}
tests := []struct {
name string
cipher cryptutil.Cipher
code string
want *pb.AuthenticateReply
wantErr bool
}{
{"good", c, ciphertext, goodReply, false},
{"bad cipher", c2, ciphertext, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Authenticate{cipher: tt.cipher}
got, err := p.Authenticate(context.Background(), &pb.AuthenticateRequest{Code: tt.code})
if (err != nil) != tt.wantErr {
t.Errorf("Authenticate.Authenticate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Authenticate.Authenticate() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -2,7 +2,6 @@ package authenticate // import "github.com/pomerium/pomerium/authenticate"
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
@ -17,16 +16,19 @@ import (
"github.com/pomerium/pomerium/internal/version"
)
// securityHeaders corresponds to HTTP response headers related to security.
// https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Headers
var securityHeaders = map[string]string{
"Strict-Transport-Security": "max-age=31536000",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Content-Security-Policy": "default-src 'none'; style-src 'self' 'sha256-pSTVzZsFAqd2U3QYu+BoBDtuJWaPM/+qMy/dBRrhb5Y='; img-src 'self';",
"Referrer-Policy": "Same-origin",
"Content-Security-Policy": "default-src 'none'; style-src 'self' " +
"'sha256-pSTVzZsFAqd2U3QYu+BoBDtuJWaPM/+qMy/dBRrhb5Y='; img-src 'self';",
"Referrer-Policy": "Same-origin",
}
// Handler returns the Http.Handlers for authentication, callback, and refresh
// Handler returns the Http.Handlers for authenticate, callback, and refresh
func (p *Authenticate) Handler() http.Handler {
// set up our standard middlewares
stdMiddleware := middleware.NewChain()
@ -52,8 +54,6 @@ func (p *Authenticate) Handler() http.Handler {
middleware.ValidateSignature(p.SharedKey),
middleware.ValidateRedirectURI(p.ProxyRootDomains))
validateClientSecretMiddleware := stdMiddleware.Append(middleware.ValidateClientSecret(p.SharedKey))
mux := http.NewServeMux()
mux.Handle("/robots.txt", stdMiddleware.ThenFunc(p.RobotsTxt))
// Identity Provider (IdP) callback endpoints and callbacks
@ -61,11 +61,7 @@ func (p *Authenticate) Handler() http.Handler {
mux.Handle("/oauth2/callback", stdMiddleware.ThenFunc(p.OAuthCallback))
// authenticate-server endpoints
mux.Handle("/sign_in", validateSignatureMiddleware.ThenFunc(p.SignIn))
mux.Handle("/sign_out", validateSignatureMiddleware.ThenFunc(p.SignOut)) // "GET", "POST"
mux.Handle("/profile", validateClientSecretMiddleware.ThenFunc(p.GetProfile)) // GET
mux.Handle("/validate", validateClientSecretMiddleware.ThenFunc(p.ValidateToken)) // GET
mux.Handle("/redeem", validateClientSecretMiddleware.ThenFunc(p.Redeem)) // POST
mux.Handle("/refresh", validateClientSecretMiddleware.ThenFunc(p.Refresh)) //POST
mux.Handle("/sign_out", validateSignatureMiddleware.ThenFunc(p.SignOut)) // GET POST
return mux
}
@ -76,43 +72,15 @@ func (p *Authenticate) RobotsTxt(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
}
// SignInPage directs the user to the sign in page. Takes a `redirect_uri` param.
func (p *Authenticate) SignInPage(w http.ResponseWriter, r *http.Request) {
redirectURL := p.RedirectURL.ResolveReference(r.URL)
destinationURL, _ := url.Parse(redirectURL.Query().Get("redirect_uri"))
t := struct {
ProviderName string
AllowedDomains []string
Redirect string
Destination string
Version string
}{
ProviderName: p.provider.Data().ProviderName,
AllowedDomains: p.AllowedDomains,
Redirect: redirectURL.String(),
Destination: destinationURL.Host,
Version: version.FullVersion(),
}
log.FromRequest(r).Debug().
Str("ProviderName", p.provider.Data().ProviderName).
Str("Redirect", redirectURL.String()).
Str("Destination", destinationURL.Host).
Str("AllowedDomains", strings.Join(p.AllowedDomains, ", ")).
Msg("authenticate: SignInPage")
w.WriteHeader(http.StatusOK)
p.templates.ExecuteTemplate(w, "sign_in.html", t)
}
func (p *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*sessions.SessionState, error) {
session, err := p.sessionStore.LoadSession(r)
if err != nil {
log.Error().Err(err).Msg("authenticate: failed to load session")
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed to load session")
p.sessionStore.ClearSession(w, r)
return nil, err
}
// ensure sessions lifetime has not expired
// if long-lived lifetime has expired, clear session
if session.LifetimePeriodExpired() {
log.FromRequest(r).Warn().Msg("authenticate: lifetime expired")
p.sessionStore.ClearSession(w, r)
@ -120,18 +88,14 @@ func (p *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*se
}
// check if session refresh period is up
if session.RefreshPeriodExpired() {
ok, err := p.provider.RefreshSessionIfNeeded(session)
newToken, err := p.provider.Refresh(session.RefreshToken)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed to refresh session")
p.sessionStore.ClearSession(w, r)
return nil, err
}
if !ok {
log.FromRequest(r).Error().Msg("user unauthorized after refresh")
p.sessionStore.ClearSession(w, r)
return nil, httputil.ErrUserNotAuthorized
}
// update refresh'd session in cookie
session.AccessToken = newToken.AccessToken
session.RefreshDeadline = newToken.Expiry
err = p.sessionStore.SaveSession(w, r, session)
if err != nil {
// We refreshed the session successfully, but failed to save it.
@ -143,9 +107,9 @@ func (p *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*se
}
} else {
// The session has not exceeded it's lifetime or requires refresh
ok := p.provider.ValidateSessionState(session)
if !ok {
log.FromRequest(r).Error().Msg("invalid session state")
ok, err := p.provider.Validate(session.IDToken)
if !ok || err != nil {
log.FromRequest(r).Error().Err(err).Msg("invalid session state")
p.sessionStore.ClearSession(w, r)
return nil, httputil.ErrUserNotAuthorized
}
@ -157,68 +121,53 @@ func (p *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*se
}
}
// authenticate really should not be in the business of authorization
// todo(bdd) : remove when authorization module added
if !p.Validator(session.Email) {
log.FromRequest(r).Error().Msg("invalid email user")
return nil, httputil.ErrUserNotAuthorized
}
log.Info().Msg("authenticate")
return session, nil
}
// SignIn handles the /sign_in endpoint. It attempts to authenticate the user,
// and if the user is not authenticated, it renders a sign in page.
func (p *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) {
// We attempt to authenticate the user. If they cannot be authenticated, we render a sign-in
// page.
//
// If the user is authenticated, we redirect back to the proxy application
// at the `redirect_uri`, with a temporary token.
//
// TODO: It is possible for a user to visit this page without a redirect destination.
// Should we allow the user to authenticate? If not, what should be the proposed workflow?
session, err := p.authenticate(w, r)
switch err {
case nil:
// User is authenticated, redirect back to proxy
p.ProxyOAuthRedirect(w, r, session)
case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession:
log.Debug().Err(err).Msg("authenticate.SignIn")
log.Info().Err(err).Msg("authenticate.SignIn : expected failure")
if err != http.ErrNoCookie {
p.sessionStore.ClearSession(w, r)
}
p.OAuthStart(w, r)
default:
log.Error().Err(err).Msg("authenticate.SignIn")
log.Error().Err(err).Msg("authenticate: unexpected sign in error")
httputil.ErrorResponse(w, r, err.Error(), httputil.CodeForError(err))
}
}
// ProxyOAuthRedirect redirects the user back to sso proxy's redirection endpoint.
// ProxyOAuthRedirect redirects the user back to proxy's redirection endpoint.
// This workflow corresponds to Section 3.1.2 of the OAuth2 RFC.
// See https://tools.ietf.org/html/rfc6749#section-3.1.2 for more specific information.
func (p *Authenticate) ProxyOAuthRedirect(w http.ResponseWriter, r *http.Request, session *sessions.SessionState) {
// This workflow corresponds to Section 3.1.2 of the OAuth2 RFC.
// See https://tools.ietf.org/html/rfc6749#section-3.1.2 for more specific information.
//
// We redirect the user back to the proxy application's redirection endpoint; in the
// sso proxy, this is the `/oauth/callback` endpoint.
//
// We must provide the proxy with a temporary authorization code via the `code` parameter,
// which they can use to redeem an access token for subsequent API calls.
//
// We must also include the original `state` parameter received from the proxy application.
err := r.ParseForm()
if err != nil {
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return
}
// original `state` parameter received from the proxy application.
state := r.Form.Get("state")
if state == "" {
httputil.ErrorResponse(w, r, "no state parameter supplied", http.StatusForbidden)
return
}
// redirect url of proxy-service
redirectURI := r.Form.Get("redirect_uri")
if redirectURI == "" {
httputil.ErrorResponse(w, r, "no redirect_uri parameter supplied", http.StatusForbidden)
@ -230,7 +179,7 @@ func (p *Authenticate) ProxyOAuthRedirect(w http.ResponseWriter, r *http.Request
httputil.ErrorResponse(w, r, "malformed redirect_uri parameter passed", http.StatusBadRequest)
return
}
// encrypt session state as json blob
encrypted, err := sessions.MarshalSession(session, p.cipher)
if err != nil {
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
@ -267,6 +216,7 @@ func (p *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
case nil:
break
case http.ErrNoCookie: // if there's no cookie in the session we can just redirect
log.Error().Err(err).Msg("authenticate.SignOut : no cookie")
http.Redirect(w, r, redirectURI, http.StatusFound)
return
default:
@ -277,7 +227,7 @@ func (p *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
return
}
err = p.provider.Revoke(session)
err = p.provider.Revoke(session.AccessToken)
if err != nil {
log.Error().Err(err).Msg("authenticate.SignOut : error revoking session")
p.SignOutPage(w, r, "An error occurred during sign out. Please try again.")
@ -299,10 +249,11 @@ func (p *Authenticate) SignOutPage(w http.ResponseWriter, r *http.Request, messa
signature := r.Form.Get("sig")
timestamp := r.Form.Get("ts")
destinationURL, _ := url.Parse(redirectURI) //checked by middleware
destinationURL, err := url.Parse(redirectURI)
// An error message indicates that an internal server error occurred
if message != "" {
if message != "" || err != nil {
log.Error().Err(err).Msg("authenticate.SignOutPage")
w.WriteHeader(http.StatusInternalServerError)
}
@ -326,8 +277,8 @@ func (p *Authenticate) SignOutPage(w http.ResponseWriter, r *http.Request, messa
p.templates.ExecuteTemplate(w, "sign_out.html", t)
}
// OAuthStart starts the authentication process by redirecting to the provider. It provides a
// `redirectURI`, allowing the provider to redirect back to the sso proxy after authentication.
// OAuthStart starts the authenticate process by redirecting to the provider. It provides a
// `redirectURI`, allowing the provider to redirect back to the sso proxy after authenticate.
func (p *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
authRedirectURL, err := url.Parse(r.URL.Query().Get("redirect_uri"))
if err != nil {
@ -339,12 +290,12 @@ func (p *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
nonce := fmt.Sprintf("%x", cryptutil.GenerateKey())
p.csrfStore.SetCSRF(w, r, nonce)
// confirm the redirect uri is from the root domain
// verify redirect uri is from the root domain
if !middleware.ValidRedirectURI(authRedirectURL.String(), p.ProxyRootDomains) {
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
return
}
// confirm proxy url is from the root domain
// verify proxy url is from the root domain
proxyRedirectURL, err := url.Parse(authRedirectURL.Query().Get("redirect_uri"))
if err != nil || !middleware.ValidRedirectURI(proxyRedirectURL.String(), p.ProxyRootDomains) {
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
@ -359,51 +310,41 @@ func (p *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
return
}
// embed authenticate service's state as the base64'd nonce and authenticate callback url
// concat base64'd nonce and authenticate url to make state
state := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", nonce, authRedirectURL.String())))
// build the provider sign in url
signInURL := p.provider.GetSignInURL(state)
http.Redirect(w, r, signInURL, http.StatusFound)
}
func (p *Authenticate) redeemCode(host, code string) (*sessions.SessionState, error) {
session, err := p.provider.Redeem(code)
if err != nil {
return nil, err
}
// if session.Email == "" {
// return nil, fmt.Errorf("no email included in session")
// }
return session, nil
}
// getOAuthCallback completes the oauth cycle from an identity provider's callback
func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) (string, error) {
// finish the oauth cycle
err := r.ParseForm()
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("authenticate: bad form on oauth callback")
return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: err.Error()}
}
errorString := r.Form.Get("error")
if errorString != "" {
log.FromRequest(r).Error().Err(err).Msg("authenticate: provider returned error")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: errorString}
}
code := r.Form.Get("code")
if code == "" {
log.FromRequest(r).Error().Err(err).Msg("authenticate: provider missing code")
return "", httputil.HTTPError{Code: http.StatusBadRequest, Message: "Missing Code"}
}
session, err := p.redeemCode(r.Host, code)
session, err := p.provider.Authenticate(code)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("error redeeming authentication code")
log.FromRequest(r).Error().Err(err).Msg("authenticate: error redeeming authenticate code")
return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: err.Error()}
}
bytes, err := base64.URLEncoding.DecodeString(r.Form.Get("state"))
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("failed decoding state")
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed decoding state")
return "", httputil.HTTPError{Code: http.StatusBadRequest, Message: "Couldn't decode state"}
}
s := strings.SplitN(string(bytes), ":", 2)
@ -414,11 +355,12 @@ func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
redirect := s[1]
c, err := p.csrfStore.GetCSRF(r)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("authenticate: bad csrf")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Missing CSRF token"}
}
p.csrfStore.ClearCSRF(w, r)
if c.Value != nonce {
log.FromRequest(r).Error().Err(err).Msg("CSRF token mismatch")
log.FromRequest(r).Error().Err(err).Msg("authenticate: csrf mismatch")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "CSRF failed"}
}
@ -427,13 +369,10 @@ func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
}
// Set cookie, or deny: validates the session email and group
// - for p.Validator see validator.go#newValidatorImpl for more info
// - for p.provider.ValidateGroup see providers/google.go#ValidateGroup for more info
if !p.Validator(session.Email) {
log.FromRequest(r).Error().Err(err).Str("email", session.Email).Msg("invalid email permissions denied")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "You don't have access"}
}
log.FromRequest(r).Info().Str("email", session.Email).Msg("authentication complete")
err = p.sessionStore.SaveSession(w, r, session)
if err != nil {
log.Error().Err(err).Msg("internal error")
@ -442,182 +381,22 @@ func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
return redirect, nil
}
// OAuthCallback handles the callback from the provider, and returns an error response if there is an error.
// If there is no error it will redirect to the redirect url.
// OAuthCallback handles the callback from the identity provider. Displays an error page if there
// was an error. If successful, redirects back to the proxy-service via the redirect-url.
func (p *Authenticate) OAuthCallback(w http.ResponseWriter, r *http.Request) {
redirect, err := p.getOAuthCallback(w, r)
switch h := err.(type) {
case nil:
break
case httputil.HTTPError:
log.Error().Err(err).Msg("authenticate: oauth callback error")
httputil.ErrorResponse(w, r, h.Message, h.Code)
return
default:
log.Error().Err(err).Msg("authenticate.OAuthCallback")
log.Error().Err(err).Msg("authenticate: unexpected oauth callback error")
httputil.ErrorResponse(w, r, "Internal Error", http.StatusInternalServerError)
return
}
// redirect back to the proxy-service
http.Redirect(w, r, redirect, http.StatusFound)
}
// Redeem has a signed access token, and provides the user information associated with the access token.
func (p *Authenticate) Redeem(w http.ResponseWriter, r *http.Request) {
// The auth code is redeemed by the sso proxy for an access token, refresh token,
// expiration, and email.
err := r.ParseForm()
if err != nil {
http.Error(w, fmt.Sprintf("Bad Request: %s", err.Error()), http.StatusBadRequest)
return
}
session, err := sessions.UnmarshalSession(r.Form.Get("code"), p.cipher)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed to unmarshal session")
http.Error(w, fmt.Sprintf("invalid auth code: %s", err.Error()), http.StatusUnauthorized)
return
}
if session == nil {
log.FromRequest(r).Error().Err(err).Msg("empty session")
http.Error(w, fmt.Sprintf("empty session: %s", err.Error()), http.StatusUnauthorized)
return
}
if session != nil && (session.RefreshPeriodExpired() || session.LifetimePeriodExpired()) {
log.FromRequest(r).Error().Msg("expired session")
p.sessionStore.ClearSession(w, r)
http.Error(w, fmt.Sprintf("expired session"), http.StatusUnauthorized)
return
}
response := struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token"`
ExpiresIn int64 `json:"expires_in"`
Email string `json:"email"`
}{
AccessToken: session.AccessToken,
RefreshToken: session.RefreshToken,
IDToken: session.IDToken,
ExpiresIn: int64(time.Until(session.RefreshDeadline).Seconds()),
Email: session.Email,
}
jsonBytes, err := json.Marshal(response)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("GAP-Auth", session.Email)
w.Header().Set("Content-Type", "application/json")
w.Write(jsonBytes)
}
// Refresh takes a refresh token and returns a new access token
func (p *Authenticate) Refresh(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, fmt.Sprintf("Bad Request: %s", err.Error()), http.StatusBadRequest)
return
}
refreshToken := r.Form.Get("refresh_token")
if refreshToken == "" {
http.Error(w, "Bad Request: No Refresh Token", http.StatusBadRequest)
return
}
accessToken, expiresIn, err := p.provider.RefreshAccessToken(refreshToken)
if err != nil {
httputil.ErrorResponse(w, r, err.Error(), httputil.CodeForError(err))
return
}
response := struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
}{
AccessToken: accessToken,
ExpiresIn: int64(expiresIn.Seconds()),
}
bytes, err := json.Marshal(response)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
w.Write(bytes)
}
// GetProfile gets a list of groups of which a user is a member.
func (p *Authenticate) GetProfile(w http.ResponseWriter, r *http.Request) {
// The sso proxy sends the user's email to this endpoint to get a list of Google groups that
// the email is a member of. The proxy will compare these groups to the list of allowed
// groups for the upstream service the user is trying to access.
email := r.FormValue("email")
if email == "" {
http.Error(w, "no email address included", http.StatusBadRequest)
return
}
// groupsFormValue := r.FormValue("groups")
// allowedGroups := []string{}
// if groupsFormValue != "" {
// allowedGroups = strings.Split(groupsFormValue, ",")
// }
// groups, err := p.provider.ValidateGroupMembership(email, allowedGroups)
// if err != nil {
// log.Error().Err(err).Msg("authenticate.GetProfile : error retrieving groups")
// httputil.ErrorResponse(w, r, err.Error(), httputil.CodeForError(err))
// return
// }
response := struct {
Email string `json:"email"`
}{
Email: email,
}
jsonBytes, err := json.Marshal(response)
if err != nil {
http.Error(w, fmt.Sprintf("error marshaling response: %s", err.Error()), http.StatusInternalServerError)
return
}
w.Header().Set("GAP-Auth", email)
w.Header().Set("Content-Type", "application/json")
w.Write(jsonBytes)
}
// ValidateToken validates the X-Access-Token from the header and returns an error response
// if it's invalid
func (p *Authenticate) ValidateToken(w http.ResponseWriter, r *http.Request) {
accessToken := r.Header.Get("X-Access-Token")
idToken := r.Header.Get("X-Id-Token")
if accessToken == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
if idToken == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
ok := p.provider.ValidateSessionState(&sessions.SessionState{
AccessToken: accessToken,
IDToken: idToken,
})
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
}

View file

@ -1,14 +1,12 @@
package authenticate
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/pomerium/pomerium/authenticate/providers"
"github.com/pomerium/pomerium/internal/templates"
)
@ -19,7 +17,6 @@ func testAuthenticate() *Authenticate {
auth.AllowedDomains = []string{"*"}
auth.ProxyRootDomains = []string{"example.com"}
auth.templates = templates.New()
auth.provider = providers.NewTestProvider(auth.RedirectURL)
return &auth
}
@ -38,43 +35,5 @@ func TestAuthenticate_RobotsTxt(t *testing.T) {
expected := fmt.Sprintf("User-agent: *\nDisallow: /")
if rr.Body.String() != expected {
t.Errorf("handler returned wrong body: got %v want %v", rr.Body.String(), expected)
}
}
func TestAuthenticate_SignInPage(t *testing.T) {
auth := testAuthenticate()
v := url.Values{}
v.Set("request_uri", "this-is-a-test-uri")
url := fmt.Sprintf("/signin?%s", v.Encode())
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(auth.SignInPage)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
body := rr.Body.Bytes()
tests := []struct {
name string
value string
want bool
}{
{"provider name", auth.provider.Data().ProviderName, true},
{"destination url", v.Encode(), true},
{"shouldn't be found", "this string should not be in the body", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := bytes.Contains(body, []byte(tt.value)); got != tt.want {
t.Errorf("handler body missing expected value %v", tt.value)
}
})
}
}

View file

@ -0,0 +1,5 @@
// Package providers implements OpenID Connect client logic for the set of supported identity
// providers.
// OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 RFC6749 protocol.
// https://openid.net/specs/openid-connect-core-1_0.html
package providers // import "github.com/pomerium/pomerium/internal/providers"

View file

@ -15,7 +15,7 @@ const defaultGitlabProviderURL = "https://gitlab.com"
// GitlabProvider is an implementation of the Provider interface.
type GitlabProvider struct {
*ProviderData
*IdentityProvider
cb *circuit.Breaker
}
@ -32,7 +32,7 @@ type GitlabProvider struct {
// - https://docs.gitlab.com/ee/integration/oauth_provider.html
// - https://docs.gitlab.com/ee/api/oauth2.html
// - https://gitlab.com/.well-known/openid-configuration
func NewGitlabProvider(p *ProviderData) (*GitlabProvider, error) {
func NewGitlabProvider(p *IdentityProvider) (*GitlabProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
p.ProviderURL = defaultGitlabProviderURL
@ -42,8 +42,9 @@ func NewGitlabProvider(p *ProviderData) (*GitlabProvider, error) {
if err != nil {
return nil, err
}
p.Scopes = []string{oidc.ScopeOpenID, "read_user"}
if len(p.Scopes) == 0 {
p.Scopes = []string{oidc.ScopeOpenID, "read_user"}
}
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{
ClientID: p.ClientID,
@ -53,7 +54,7 @@ func NewGitlabProvider(p *ProviderData) (*GitlabProvider, error) {
Scopes: p.Scopes,
}
gitlabProvider := &GitlabProvider{
ProviderData: p,
IdentityProvider: p,
}
gitlabProvider.cb = circuit.NewBreaker(&circuit.Options{
HalfOpenConcurrentRequests: 2,

View file

@ -11,7 +11,6 @@ import (
"github.com/pomerium/pomerium/authenticate/circuit"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version"
)
@ -19,14 +18,14 @@ const defaultGoogleProviderURL = "https://accounts.google.com"
// GoogleProvider is an implementation of the Provider interface.
type GoogleProvider struct {
*ProviderData
*IdentityProvider
cb *circuit.Breaker
// non-standard oidc fields
RevokeURL *url.URL
}
// NewGoogleProvider returns a new GoogleProvider and sets the provider url endpoints.
func NewGoogleProvider(p *ProviderData) (*GoogleProvider, error) {
func NewGoogleProvider(p *IdentityProvider) (*GoogleProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
@ -37,18 +36,20 @@ func NewGoogleProvider(p *ProviderData) (*GoogleProvider, error) {
if err != nil {
return nil, err
}
if len(p.Scopes) == 0 {
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email"}
}
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
Scopes: p.Scopes,
}
googleProvider := &GoogleProvider{
ProviderData: p,
IdentityProvider: p,
}
// google supports a revocation endpoint
var claims struct {
@ -91,9 +92,9 @@ func (p *GoogleProvider) cbStateChange(from, to circuit.State) {
//
// https://developers.google.com/identity/protocols/OAuth2WebServer#tokenrevoke
// https://github.com/googleapis/google-api-dotnet-client/issues/1285
func (p *GoogleProvider) Revoke(s *sessions.SessionState) error {
func (p *GoogleProvider) Revoke(accessToken string) error {
params := url.Values{}
params.Add("token", s.AccessToken)
params.Add("token", accessToken)
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil)
if err != nil && err != httputil.ErrTokenRevoked {
return err
@ -105,4 +106,5 @@ func (p *GoogleProvider) Revoke(s *sessions.SessionState) error {
// Google requires access type offline
func (p *GoogleProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
}

View file

@ -11,7 +11,6 @@ import (
"github.com/pomerium/pomerium/authenticate/circuit"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version"
)
@ -22,7 +21,7 @@ const defaultAzureProviderURL = "https://login.microsoftonline.com/common"
// AzureProvider is an implementation of the Provider interface
type AzureProvider struct {
*ProviderData
*IdentityProvider
cb *circuit.Breaker
// non-standard oidc fields
RevokeURL *url.URL
@ -31,7 +30,7 @@ type AzureProvider struct {
// NewAzureProvider returns a new AzureProvider and sets the provider url endpoints.
// If non-"common" tenant is desired, ProviderURL must be set.
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc
func NewAzureProvider(p *ProviderData) (*AzureProvider, error) {
func NewAzureProvider(p *IdentityProvider) (*AzureProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
@ -43,18 +42,20 @@ func NewAzureProvider(p *ProviderData) (*AzureProvider, error) {
if err != nil {
return nil, err
}
if len(p.Scopes) == 0 {
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "offline_access"}
}
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
Scopes: p.Scopes,
}
azureProvider := &AzureProvider{
ProviderData: p,
IdentityProvider: p,
}
// azure has a "end session endpoint"
var claims struct {
@ -95,9 +96,9 @@ func (p *AzureProvider) cbStateChange(from, to circuit.State) {
// Revoke revokes the access token a given session state.
//https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
func (p *AzureProvider) Revoke(s *sessions.SessionState) error {
func (p *AzureProvider) Revoke(token string) error {
params := url.Values{}
params.Add("token", s.AccessToken)
params.Add("token", token)
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil)
if err != nil && err != httputil.ErrTokenRevoked {
return err

View file

@ -12,11 +12,11 @@ import (
// of an authorization identity provider.
// see : https://openid.net/specs/openid-connect-core-1_0.html
type OIDCProvider struct {
*ProviderData
*IdentityProvider
}
// NewOIDCProvider creates a new instance of an OpenID Connect provider.
func NewOIDCProvider(p *ProviderData) (*OIDCProvider, error) {
func NewOIDCProvider(p *IdentityProvider) (*OIDCProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
return nil, errors.New("missing required provider url")
@ -26,13 +26,16 @@ func NewOIDCProvider(p *ProviderData) (*OIDCProvider, error) {
if err != nil {
return nil, err
}
if len(p.Scopes) == 0 {
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "offline_access"}
}
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
Scopes: p.Scopes,
}
return &OIDCProvider{ProviderData: p}, nil
return &OIDCProvider{IdentityProvider: p}, nil
}

View file

@ -9,21 +9,20 @@ import (
"golang.org/x/oauth2"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version"
)
// OktaProvider provides a standard, OpenID Connect implementation
// of an authorization identity provider.
type OktaProvider struct {
*ProviderData
*IdentityProvider
// non-standard oidc fields
RevokeURL *url.URL
}
// NewOktaProvider creates a new instance of an OpenID Connect provider.
func NewOktaProvider(p *ProviderData) (*OktaProvider, error) {
func NewOktaProvider(p *IdentityProvider) (*OktaProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
return nil, errors.New("missing required provider url")
@ -33,24 +32,26 @@ func NewOktaProvider(p *ProviderData) (*OktaProvider, error) {
if err != nil {
return nil, err
}
if len(p.Scopes) == 0 {
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "offline_access"}
}
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
Scopes: p.Scopes,
}
oktaProvider := OktaProvider{ProviderData: p}
// okta supports a revocation endpoint
var claims struct {
RevokeURL string `json:"revocation_endpoint"`
}
if err := p.provider.Claims(&claims); err != nil {
return nil, err
}
oktaProvider := OktaProvider{IdentityProvider: p}
oktaProvider.RevokeURL, err = url.Parse(claims.RevokeURL)
if err != nil {
@ -61,11 +62,11 @@ func NewOktaProvider(p *ProviderData) (*OktaProvider, error) {
// Revoke revokes the access token a given session state.
// https://developer.okta.com/docs/api/resources/oidc#revoke
func (p *OktaProvider) Revoke(s *sessions.SessionState) error {
func (p *OktaProvider) Revoke(token string) error {
params := url.Values{}
params.Add("client_id", p.ClientID)
params.Add("client_secret", p.ClientSecret)
params.Add("token", s.IDToken)
params.Add("token", token)
params.Add("token_type_hint", "refresh_token")
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil)
if err != nil && err != httputil.ErrTokenRevoked {
@ -73,3 +74,9 @@ func (p *OktaProvider) Revoke(s *sessions.SessionState) 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)
}

View file

@ -1,12 +1,11 @@
//go:generate protoc -I ../../proto/authenticate --go_out=plugins=grpc:../../proto/authenticate ../../proto/authenticate/authenticate.proto
package providers // import "github.com/pomerium/pomerium/internal/providers"
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
@ -32,21 +31,17 @@ const (
// Provider is an interface exposing functions necessary to interact with a given provider.
type Provider interface {
Data() *ProviderData
Redeem(string) (*sessions.SessionState, error)
ValidateSessionState(*sessions.SessionState) bool
Authenticate(string) (*sessions.SessionState, error)
Validate(string) (bool, error)
Refresh(string) (*oauth2.Token, error)
Revoke(string) error
GetSignInURL(state string) string
RefreshSessionIfNeeded(*sessions.SessionState) (bool, error)
Revoke(*sessions.SessionState) error
RefreshAccessToken(string) (string, time.Duration, error)
}
// New returns a new identity provider based given its name.
// Returns an error if selected provided not found or if the provider fails to instantiate.
func New(provider string, pd *ProviderData) (Provider, error) {
var err error
var p Provider
switch provider {
// Returns an error if selected provided not found or if the identity provider is not known.
func New(providerName string, pd *IdentityProvider) (p Provider, err error) {
switch providerName {
case AzureProviderName:
p, err = NewAzureProvider(pd)
case GitlabProviderName:
@ -58,7 +53,7 @@ func New(provider string, pd *ProviderData) (Provider, error) {
case OktaProviderName:
p, err = NewOktaProvider(pd)
default:
return nil, fmt.Errorf("authenticate: provider %q not found", provider)
return nil, fmt.Errorf("authenticate: %q name not found", providerName)
}
if err != nil {
return nil, err
@ -66,11 +61,13 @@ func New(provider string, pd *ProviderData) (Provider, error) {
return p, nil
}
// ProviderData holds the fields associated with providers
// necessary to implement the Provider interface.
type ProviderData struct {
// IdentityProvider contains the fields required for an OAuth 2.0 Authorization Request that
// requests that the End-User be authenticated by the Authorization Server.
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
type IdentityProvider struct {
ProviderName string
RedirectURL *url.URL
ProviderName string
ClientID string
ClientSecret string
ProviderURL string
@ -82,118 +79,50 @@ type ProviderData struct {
oauth *oauth2.Config
}
// Data returns a ProviderData.
func (p *ProviderData) Data() *ProviderData { return p }
// GetSignInURL returns the sign in url with typical oauth parameters
func (p *ProviderData) GetSignInURL(state string) string {
// GetSignInURL returns a URL to OAuth 2.0 provider's consent page
// that asks for permissions for the required scopes explicitly.
//
// State is a token to protect the user from CSRF attacks. You must
// always provide a non-empty string and validate that it matches the
// the state query parameter on your redirect callback.
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
func (p *IdentityProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state)
}
// ValidateSessionState validates a given session's from it's JWT token
// Validate validates a given session's from it's JWT token
// The function verifies it's been signed by the provider, preforms
// any additional checks depending on the Config, and returns the payload.
//
// ValidateSessionState does NOT do nonce validation.
func (p *ProviderData) ValidateSessionState(s *sessions.SessionState) bool {
// Validate does NOT do nonce validation.
// Validate does NOT check if revoked.
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func (p *IdentityProvider) Validate(idToken string) (bool, error) {
ctx := context.Background()
_, err := p.verifier.Verify(ctx, s.IDToken)
_, err := p.verifier.Verify(ctx, idToken)
if err != nil {
log.Error().Err(err).Msg("authenticate/providers: failed to verify session state")
return false
return false, err
}
return true
}
// Redeem creates a session with an identity provider from a authorization code
func (p *ProviderData) Redeem(code string) (*sessions.SessionState, error) {
ctx := context.Background()
// convert authorization code into a token
token, err := p.oauth.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("authenticate/providers: failed token exchange: %v", err)
}
s, err := p.createSessionState(ctx, token)
if err != nil {
return nil, fmt.Errorf("authenticate/providers: unable to update session: %v", err)
}
// check if provider has info endpoint, try to hit that and gather more info
// especially useful if initial request did not contain email
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
var claims struct {
UserInfoURL string `json:"userinfo_endpoint"`
}
if err := p.provider.Claims(&claims); err != nil || claims.UserInfoURL == "" {
log.Error().Err(err).Msg("authenticate/providers: failed retrieving userinfo_endpoint")
} else {
// userinfo endpoint found and valid
userInfo, err := p.UserInfo(ctx, claims.UserInfoURL, oauth2.StaticTokenSource(token))
if err != nil {
return nil, fmt.Errorf("authenticate/providers: can't parse userinfo_endpoint: %v", err)
}
s.Email = userInfo.Email
}
return s, nil
}
// RefreshSessionIfNeeded will refresh the session state if it's deadline is expired
func (p *ProviderData) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
if !sessionRefreshRequired(s) {
log.Debug().Msg("authenticate/providers: session refresh not needed")
return false, nil
}
origExpiration := s.RefreshDeadline
err := p.redeemRefreshToken(s)
if err != nil {
return false, fmt.Errorf("authenticate/providers: couldn't refresh token: %v", err)
}
log.Debug().Time("NewDeadline", s.RefreshDeadline).Time("OldDeadline", origExpiration).Msgf("authenticate/providers refreshed")
return true, nil
}
func (p *ProviderData) redeemRefreshToken(s *sessions.SessionState) error {
log.Info().Msg("authenticate/providers.oidc.redeemRefreshToken 1")
// Authenticate creates a session with an identity provider from a authorization code
func (p *IdentityProvider) Authenticate(code string) (*sessions.SessionState, error) {
ctx := context.Background()
t := &oauth2.Token{
RefreshToken: s.RefreshToken,
Expiry: time.Now().Add(-time.Hour),
}
log.Info().Msg("authenticate/providers.oidc.redeemRefreshToken 3")
// returns a TokenSource automatically refreshing it as necessary using the provided context
token, err := p.oauth.TokenSource(ctx, t).Token()
// convert authorization code into a token
oauth2Token, err := p.oauth.Exchange(ctx, code)
if err != nil {
return fmt.Errorf("authenticate/providers: failed to get token: %v", err)
return nil, fmt.Errorf("authenticate/providers: failed token exchange: %v", err)
}
log.Info().Msg("authenticate/providers.oidc.redeemRefreshToken 4")
newSession, err := p.createSessionState(ctx, token)
if err != nil {
return fmt.Errorf("authenticate/providers: unable to update session: %v", err)
}
s.AccessToken = newSession.AccessToken
s.IDToken = newSession.IDToken
s.RefreshToken = newSession.RefreshToken
s.RefreshDeadline = newSession.RefreshDeadline
s.Email = newSession.Email
log.Info().
Str("AccessToken", s.AccessToken).
Str("IdToken", s.IDToken).
Time("RefreshDeadline", s.RefreshDeadline).
Str("RefreshToken", s.RefreshToken).
Str("Email", s.Email).
Msg("authenticate/providers.redeemRefreshToken")
Str("RefreshToken", oauth2Token.RefreshToken).
Str("TokenType", oauth2Token.TokenType).
Str("AccessToken", oauth2Token.AccessToken).
Msg("Authenticate - oauth.Exchange")
return nil
}
func (p *ProviderData) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
rawIDToken, ok := token.Extra("id_token").(string)
//id_token contains claims about the authenticated user
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("token response did not contain an id_token")
}
@ -204,142 +133,51 @@ func (p *ProviderData) createSessionState(ctx context.Context, token *oauth2.Tok
return nil, fmt.Errorf("authenticate/providers: could not verify id_token: %v", err)
}
// Extract custom claims.
// Extract id_token which contains claims about the authenticated user
var claims struct {
Email string `json:"email"`
Verified *bool `json:"email_verified"`
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("authenticate/providers: failed to parse id_token claims: %v", err)
}
log.Debug().
Str("AccessToken", token.AccessToken).
Str("IDToken", rawIDToken).
Str("claims.Email", claims.Email).
Str("RefreshToken", token.RefreshToken).
Str("idToken.Subject", idToken.Subject).
Str("idToken.Nonce", idToken.Nonce).
Str("RefreshDeadline", idToken.Expiry.String()).
Str("LifetimeDeadline", idToken.Expiry.String()).
Msg("authenticate/providers.createSessionState")
return &sessions.SessionState{
AccessToken: token.AccessToken,
IDToken: rawIDToken,
RefreshToken: token.RefreshToken,
RefreshDeadline: idToken.Expiry,
LifetimeDeadline: idToken.Expiry,
AccessToken: oauth2Token.AccessToken,
RefreshToken: oauth2Token.RefreshToken,
RefreshDeadline: oauth2Token.Expiry,
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
Email: claims.Email,
User: idToken.Subject,
Groups: claims.Groups,
}, nil
}
// RefreshAccessToken allows the service to refresh an access token without
// prompting the user for permission.
func (p *ProviderData) RefreshAccessToken(refreshToken string) (string, time.Duration, error) {
// Refresh renews a user's session using an access token without reprompting the user.
func (p *IdentityProvider) Refresh(refreshToken string) (*oauth2.Token, error) {
if refreshToken == "" {
return "", 0, errors.New("authenticate/providers: missing refresh token")
}
ctx := context.Background()
c := oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: oauth2.Endpoint{TokenURL: p.ProviderURL},
return nil, errors.New("authenticate/providers: missing refresh token")
}
t := oauth2.Token{RefreshToken: refreshToken}
ts := c.TokenSource(ctx, &t)
newToken, err := p.oauth.TokenSource(context.Background(), &t).Token()
if err != nil {
log.Error().Err(err).Msg("authenticate/providers.Refresh")
return nil, err
}
log.Info().
Str("RefreshToken", refreshToken).
Msg("authenticate/providers.RefreshAccessToken")
Str("newToken.AccessToken", newToken.AccessToken).
Str("time.Until(newToken.Expiry)", time.Until(newToken.Expiry).String()).
Msg("authenticate/providers.Refresh")
newToken, err := ts.Token()
if err != nil {
log.Error().Err(err).Msg("authenticate/providers.RefreshAccessToken")
return "", 0, err
}
return newToken.AccessToken, time.Until(newToken.Expiry), nil
return newToken, nil
}
// Revoke enables a user to revoke her token. If the identity provider supports revocation
// the endpoint is available, otherwise an error is thrown.
func (p *ProviderData) Revoke(s *sessions.SessionState) error {
func (p *IdentityProvider) Revoke(token string) error {
return errors.New("authenticate/providers: revoke not implemented")
}
func sessionRefreshRequired(s *sessions.SessionState) bool {
return s == nil || s.RefreshDeadline.After(time.Now()) || s.RefreshToken == ""
}
// UserInfo represents the OpenID Connect userinfo claims.
// see: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
type UserInfo struct {
// Stanard OIDC User fields
Subject string `json:"sub"`
Profile string `json:"profile"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
// custom claims
Name string `json:"name"` // google, gitlab
GivenName string `json:"given_name"` // google
FamilyName string `json:"family_name"` // google
Picture string `json:"picture"` // google,gitlab
Locale string `json:"locale"` // google
Groups []string `json:"groups"` // gitlab
claims []byte
}
// Claims unmarshals the raw JSON object claims into the provided object.
func (u *UserInfo) Claims(v interface{}) error {
if u.claims == nil {
return errors.New("authenticate/providers: claims not set")
}
return json.Unmarshal(u.claims, v)
}
// UserInfo uses the token source to query the provider's user info endpoint.
func (p *ProviderData) UserInfo(ctx context.Context, uri string, tokenSource oauth2.TokenSource) (*UserInfo, error) {
if uri == "" {
return nil, errors.New("authenticate/providers: user info endpoint is not supported by this provider")
}
req, err := http.NewRequest(http.MethodGet, uri, nil)
if err != nil {
return nil, fmt.Errorf("authenticate/providers: create GET request: %v", err)
}
token, err := tokenSource.Token()
if err != nil {
return nil, fmt.Errorf("authenticate/providers: get access token: %v", err)
}
token.SetAuthHeader(req)
resp, err := doRequest(ctx, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: %s", resp.Status, body)
}
var userInfo UserInfo
if err := json.Unmarshal(body, &userInfo); err != nil {
return nil, fmt.Errorf("authenticate/providers failed to decode userinfo: %v", err)
}
userInfo.claims = body
return &userInfo, nil
}
func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
client := http.DefaultClient
if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
client = c
}
return client.Do(req.WithContext(ctx))
}

View file

@ -1,142 +0,0 @@
package providers // import "github.com/pomerium/pomerium/internal/providers"
import (
"errors"
"fmt"
"time"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/singleflight"
)
var (
_ Provider = &SingleFlightProvider{}
)
// ErrUnexpectedReturnType is an error for an unexpected return type
var (
ErrUnexpectedReturnType = errors.New("received unexpected return type from single flight func call")
)
// SingleFlightProvider middleware provider that multiple requests for the same object
// to be processed as a single request. This is often called request collapsing or coalesce.
// This middleware leverages the golang singlelflight provider, with modifications for metrics.
//
// It's common among HTTP reverse proxy cache servers such as nginx, Squid or Varnish - they all call it something else but works similarly.
//
// * https://www.varnish-cache.org/docs/3.0/tutorial/handling_misbehaving_servers.html
// * http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_lock
// * http://wiki.squid-cache.org/Features/CollapsedForwarding
type SingleFlightProvider struct {
provider Provider
single *singleflight.Group
}
// NewSingleFlightProvider returns a new SingleFlightProvider
func NewSingleFlightProvider(provider Provider) *SingleFlightProvider {
return &SingleFlightProvider{
provider: provider,
single: &singleflight.Group{},
}
}
func (p *SingleFlightProvider) do(endpoint, key string, fn func() (interface{}, error)) (interface{}, error) {
compositeKey := fmt.Sprintf("%s/%s", endpoint, key)
resp, _, err := p.single.Do(compositeKey, fn)
return resp, err
}
// Data returns the provider data
func (p *SingleFlightProvider) Data() *ProviderData {
return p.provider.Data()
}
// Redeem wraps the provider's Redeem function.
func (p *SingleFlightProvider) Redeem(code string) (*sessions.SessionState, error) {
return p.provider.Redeem(code)
}
// ValidateSessionState wraps the provider's ValidateSessionState in a single flight call.
func (p *SingleFlightProvider) ValidateSessionState(s *sessions.SessionState) bool {
response, err := p.do("ValidateSessionState", s.AccessToken, func() (interface{}, error) {
valid := p.provider.ValidateSessionState(s)
return valid, nil
})
if err != nil {
return false
}
valid, ok := response.(bool)
if !ok {
return false
}
return valid
}
// GetSignInURL calls the provider's GetSignInURL function.
func (p *SingleFlightProvider) GetSignInURL(finalRedirect string) string {
return p.provider.GetSignInURL(finalRedirect)
}
// RefreshSessionIfNeeded wraps the provider's RefreshSessionIfNeeded function in a single flight
// call.
func (p *SingleFlightProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
response, err := p.do("RefreshSessionIfNeeded", s.RefreshToken, func() (interface{}, error) {
return p.provider.RefreshSessionIfNeeded(s)
})
if err != nil {
return false, err
}
r, ok := response.(bool)
if !ok {
return false, ErrUnexpectedReturnType
}
return r, nil
}
// Revoke wraps the provider's Revoke function in a single flight call.
func (p *SingleFlightProvider) Revoke(s *sessions.SessionState) error {
_, err := p.do("Revoke", s.AccessToken, func() (interface{}, error) {
err := p.provider.Revoke(s)
return nil, err
})
return err
}
// RefreshAccessToken wraps the provider's RefreshAccessToken function in a single flight call.
func (p *SingleFlightProvider) RefreshAccessToken(refreshToken string) (string, time.Duration, error) {
type Response struct {
AccessToken string
ExpiresIn time.Duration
}
response, err := p.do("RefreshAccessToken", refreshToken, func() (interface{}, error) {
accessToken, expiresIn, err := p.provider.RefreshAccessToken(refreshToken)
if err != nil {
return nil, err
}
return &Response{
AccessToken: accessToken,
ExpiresIn: expiresIn,
}, nil
})
if err != nil {
return "", 0, err
}
r, ok := response.(*Response)
if !ok {
return "", 0, ErrUnexpectedReturnType
}
return r.AccessToken, r.ExpiresIn, nil
}
// // Stop calls the provider's stop function
// func (p *SingleFlightProvider) Stop() {
// p.provider.Stop()
// }

View file

@ -1,77 +0,0 @@
package providers // import "github.com/pomerium/pomerium/internal/providers"
import (
"net/url"
"time"
"github.com/pomerium/pomerium/internal/sessions"
)
// TestProvider is a test implementation of the Provider interface.
type TestProvider struct {
*ProviderData
ValidToken bool
ValidGroup bool
SignInURL string
Refresh bool
RefreshFunc func(string) (string, time.Duration, error)
RefreshError error
Session *sessions.SessionState
RedeemError error
RevokeError error
Groups []string
GroupsError error
GroupsCall int
}
// NewTestProvider creates a new mock test provider.
func NewTestProvider(providerURL *url.URL) *TestProvider {
host := &url.URL{
Scheme: "http",
Host: providerURL.Host,
Path: "/authorize",
}
return &TestProvider{
ProviderData: &ProviderData{
ProviderName: "Test Provider",
ProviderURL: host.String(),
},
}
}
// ValidateSessionState returns the mock provider's ValidToken field value.
func (tp *TestProvider) ValidateSessionState(*sessions.SessionState) bool {
return tp.ValidToken
}
// GetSignInURL returns the mock provider's SignInURL field value.
func (tp *TestProvider) GetSignInURL(finalRedirect string) string {
return tp.SignInURL
}
// RefreshSessionIfNeeded returns the mock provider's Refresh value, or an error.
func (tp *TestProvider) RefreshSessionIfNeeded(*sessions.SessionState) (bool, error) {
return tp.Refresh, tp.RefreshError
}
// RefreshAccessToken returns the mock provider's refresh access token information
func (tp *TestProvider) RefreshAccessToken(s string) (string, time.Duration, error) {
return tp.RefreshFunc(s)
}
// Revoke returns nil
func (tp *TestProvider) Revoke(*sessions.SessionState) error {
return tp.RevokeError
}
// ValidateGroupMembership returns the mock provider's GroupsError if not nil, or the Groups field value.
func (tp *TestProvider) ValidateGroupMembership(string, []string) ([]string, error) {
return tp.Groups, tp.GroupsError
}
// Redeem returns the mock provider's Session and RedeemError field value.
func (tp *TestProvider) Redeem(code string) (*sessions.SessionState, error) {
return tp.Session, tp.RedeemError
}

View file

@ -6,11 +6,15 @@ import (
"net/http"
"os"
"google.golang.org/grpc"
"github.com/pomerium/pomerium/authenticate"
"github.com/pomerium/pomerium/internal/https"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/options"
"github.com/pomerium/pomerium/internal/version"
pb "github.com/pomerium/pomerium/proto/authenticate"
"github.com/pomerium/pomerium/proxy"
)
@ -34,6 +38,10 @@ func main() {
}
log.Info().Str("version", version.FullVersion()).Msg("cmd/pomerium")
grpcAuth := middleware.NewSharedSecretCred(mainOpts.SharedKey)
grpcOpts := []grpc.ServerOption{grpc.UnaryInterceptor(grpcAuth.ValidateRequest)}
grpcServer := grpc.NewServer(grpcOpts...)
var authenticateService *authenticate.Authenticate
var authHost string
if mainOpts.Services == "all" || mainOpts.Services == "authenticate" {
@ -51,6 +59,8 @@ func main() {
log.Fatal().Err(err).Msg("cmd/pomerium: new authenticate")
}
authHost = authOpts.RedirectURL.Host
pb.RegisterAuthenticatorServer(grpcServer, authenticateService)
}
var proxyService *proxy.Proxy
@ -64,6 +74,7 @@ func main() {
if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: new proxy")
}
defer proxyService.AuthenticateConn.Close()
}
topMux := http.NewServeMux()
@ -85,5 +96,7 @@ func main() {
CertFile: mainOpts.CertFile,
KeyFile: mainOpts.KeyFile,
}
log.Fatal().Err(https.ListenAndServeTLS(httpOpts, topMux)).Msg("cmd/pomerium: https serve failure")
log.Fatal().Err(https.ListenAndServeTLS(httpOpts, topMux, grpcServer)).Msg("cmd/pomerium: https serve failure")
}

View file

@ -1,6 +1,7 @@
package main // import "github.com/pomerium/pomerium/cmd/pomerium"
import (
"errors"
"fmt"
"github.com/pomerium/envconfig"
@ -14,6 +15,11 @@ type Options struct {
// Debug enables more verbose logging, and outputs human-readable logs to Stdout.
// Set with POMERIUM_DEBUG
Debug bool `envconfig:"POMERIUM_DEBUG"`
// SharedKey is the shared secret authorization key used to mutually authenticate
// requests between services.
SharedKey string `envconfig:"SHARED_SECRET"`
// Services is a list enabled service mode. If none are selected, "all" is used.
// Available options are : "all", "authenticate", "proxy".
Services string `envconfig:"SERVICES"`
@ -33,7 +39,7 @@ var defaultOptions = &Options{
Services: "all",
}
// optionsFromEnvConfig builds the authentication service's configuration
// optionsFromEnvConfig builds the IdentityProvider service's configuration
// options from provided environmental variables
func optionsFromEnvConfig() (*Options, error) {
o := defaultOptions
@ -43,6 +49,9 @@ func optionsFromEnvConfig() (*Options, error) {
if !isValidService(o.Services) {
return nil, fmt.Errorf("%s is an invalid service type", o.Services)
}
if o.SharedKey == "" {
return nil, errors.New("shared-key cannot be empty")
}
return o, nil
}

View file

@ -6,10 +6,9 @@ import (
"testing"
)
func init() {
os.Clearenv()
}
func Test_optionsFromEnvConfig(t *testing.T) {
good := defaultOptions
good.SharedKey = "test"
tests := []struct {
name string
want *Options
@ -17,18 +16,25 @@ func Test_optionsFromEnvConfig(t *testing.T) {
envValue string
wantErr bool
}{
{"good default with no env settings", defaultOptions, "", "", false},
{"good service", defaultOptions, "SERVICES", "all", false},
{"good default with no env settings", good, "", "", false},
{"invalid service type", nil, "SERVICES", "invalid", true},
{"good service", good, "SERVICES", "all", false},
{"bad debug boolean", nil, "POMERIUM_DEBUG", "yes", true},
{"missing shared secret", nil, "SHARED_SECRET", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Clearenv()
if tt.envKey != "" {
os.Setenv(tt.envKey, tt.envValue)
defer os.Unsetenv(tt.envKey)
}
if tt.envKey != "SHARED_SECRET" {
os.Setenv("SHARED_SECRET", "test")
}
got, err := optionsFromEnvConfig()
os.Unsetenv(tt.envKey)
if (err != nil) != tt.wantErr {
t.Errorf("optionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr)
return

View file

@ -34,18 +34,29 @@ Uses the [latest pomerium build](https://hub.docker.com/r/pomerium/pomerium) fro
- Minimal container-based configuration.
- Docker and Docker-Compose based.
- Uses pre-configured built-in nginx load balancer
- Runs separate containers for each service
- Comes with a pre-configured instance of on-prem Gitlab-CE
- Runs a single container for all pomerium services
- Routes default to on-prem [helloworld], [httpbin] containers.
Customize for your identity provider run `docker-compose up -f basic.docker-compose.yml`
#### basic.docker-compose.yml
<<< @/docs/docs/examples/basic.docker-compose.yml
<<< @/docs/docs/examples/docker/basic.docker-compose.yml
### Gitlab On-premise
### NGINX micro-services
- Docker and Docker-Compose based.
- Uses pre-configured built-in nginx load balancer
- Runs separate containers for each service
- Routes default to on-prem [helloworld], [httpbin], and [gitlab] containers.
Customize for your identity provider run `docker-compose up -f gitlab.docker-compose.yml`
#### nginx.docker-compose.yml
<<< @/docs/docs/examples/docker/nginx.docker-compose.yml
### Gitlab On-Prem
- Docker and Docker-Compose based.
- Uses pre-configured built-in nginx load balancer
@ -57,7 +68,7 @@ Customize for your identity provider run `docker-compose up -f gitlab.docker-com
#### gitlab.docker-compose.yml
<<< @/docs/docs/examples/gitlab.docker-compose.yml
<<< @/docs/docs/examples/docker/gitlab.docker-compose.yml
## Kubernetes

View file

@ -1,100 +0,0 @@
# Example Pomerium configuration.
#
# NOTE! Change IDP_* settings to match your identity provider settings!
# NOTE! Generate new SHARED_SECRET and COOKIE_SECRET keys!
# NOTE! Replace `corp.beyondperimeter.com` with whatever your domain is
# NOTE! Make sure certificate files (cert.pem/privkey.pem) are in the same directory as this file
# NOTE! Wrap URLs in quotes to avoid parse errors
version: "3"
services:
# NGINX routes to pomerium's services depending on the request.
nginx-proxy:
image: jwilder/nginx-proxy:latest
ports:
- "443:443"
volumes:
# NOTE!!! : nginx must be supplied with your wildcard certificates. And it expects
# it in the format of whatever your wildcard domain name is in.
# see : https://github.com/jwilder/nginx-proxy#wildcard-certificates
# So, if your subdomain is corp.beyondperimeter.com, you'd have the following :
- ./cert.pem:/etc/nginx/certs/corp.beyondperimeter.com.crt:ro
- ./privkey.pem:/etc/nginx/certs/corp.beyondperimeter.com.key:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
pomerium-authenticate:
image: pomerium/pomerium:latest # or `build: .` to build from source
environment:
- SERVICES=authenticate
# auth settings
- REDIRECT_URL=https://sso-auth.corp.beyondperimeter.com/oauth2/callback
# Identity Provider Settings (Must be changed!)
- IDP_PROVIDER="google"
- IDP_PROVIDER_URL="https://accounts.google.com"
- IDP_CLIENT_ID=851877082059-bfgkpj09noog7as3gpc3t7r6n9sjbgs6.apps.googleusercontent.com
- IDP_CLIENT_SECRET=P34wwijKRNP3skP5ag5I12kz
- SCOPE="openid email"
- PROXY_ROOT_DOMAIN=beyondperimeter.com
- ALLOWED_DOMAINS=*
# shared service settings
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
# if passing certs as files
# - CERTIFICATE_KEY=corp.beyondperimeter.com.crt
# - CERTIFICATE_KEY_FILE=corp.beyondperimeter.com.key
# Or, you can pass certifcates as bas64 encoded values. e.g. `base64 -i cert.pem`
# - CERTIFICATE=
# - CERTIFICATE_KEY=
# nginx settings
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=sso-auth.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
expose:
- 443
pomerium-proxy:
image: pomerium/pomerium:latest # or `build: .` to build from source
environment:
- SERVICES=proxy
# proxy settings
- AUTHENTICATE_SERVICE_URL=https://sso-auth.corp.beyondperimeter.com
- ROUTES=https://httpbin.corp.beyondperimeter.com=http://httpbin,https://hello.corp.beyondperimeter.com=http://hello-world/
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
# If set, a JWT based signature is appended to each request header `x-pomerium-jwt-assertion`
# - SIGNING_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
# if passing certs as files
# - CERTIFICATE_KEY=corp.beyondperimeter.com.crt
# - CERTIFICATE_KEY_FILE=corp.beyondperimeter.com.key
# Or, you can pass certifcates as bas64 encoded values. e.g. `base64 -i cert.pem`
# - CERTIFICATE=
# - CERTIFICATE_KEY=
# nginx settings
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=*.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
expose:
- 443
# https://httpbin.corp.beyondperimeter.com
httpbin:
image: kennethreitz/httpbin:latest
expose:
- 80
# Simple hello world
# https://hello.corp.beyondperimeter.com
hello-world:
image: tutum/hello-world:latest
expose:
- 80

View file

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

View file

@ -2,7 +2,7 @@ version: "3"
services:
nginx:
image: jwilder/nginx-proxy:latest
image: pomerium/nginx-proxy:latest
ports:
- "443:443"
volumes:
@ -17,18 +17,17 @@ services:
pomerium-authenticate:
build: .
restart: always
depends_on:
- "gitlab"
environment:
- POMERIUM_DEBUG=true
- SERVICES=authenticate
# auth settings
- REDIRECT_URL=https://sso-auth.corp.beyondperimeter.com/oauth2/callback
- IDP_PROVIDER="gitlab"
- IDP_PROVIDER_URL=https://gitlab.corp.beyondperimeter.com
- IDP_CLIENT_ID=022dbbd09402441dc7af1924b679bc5e6f5bf0d7a555e55b38c51e2e4e6cee76
- IDP_CLIENT_SECRET=fb7598c520c346915ee369eee57688938fe4f31329a308c4669074da562714b2
- PROXY_ROOT_DOMAIN=beyondperimeter.com
- REDIRECT_URL=https://auth.corp.beyondperimeter.com/oauth2/callback
# Identity Provider Settings (Must be changed!)
- IDP_PROVIDER=google
- IDP_PROVIDER_URL=https://accounts.google.com
- IDP_CLIENT_ID=REPLACEME
- IDP_CLIENT_SECRET=REPLACE_ME
- PROXY_ROOT_DOMAIN=corp.beyondperimeter.com
- ALLOWED_DOMAINS=*
- SKIP_PROVIDER_BUTTON=false
# shared service settings
@ -36,14 +35,13 @@ services:
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=sso-auth.corp.beyondperimeter.com
- VIRTUAL_HOST=auth.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
expose:
- 443
pomerium-proxy:
build: .
restart: always
@ -51,12 +49,17 @@ services:
- POMERIUM_DEBUG=true
- SERVICES=proxy
# proxy settings
- AUTHENTICATE_SERVICE_URL=https://sso-auth.corp.beyondperimeter.com
- ROUTES=https://httpbin.corp.beyondperimeter.com=http://httpbin,https://hello.corp.beyondperimeter.com=http://hello-world/
- AUTHENTICATE_SERVICE_URL=https://auth.corp.beyondperimeter.com
# IMPORTANT! If you are running pomerium behind another ingress (loadbalancer/firewall/etc)
# you must tell pomerium proxy how to communicate using an internal hostname for RPC
- AUTHENTICATE_INTERNAL_URL=pomerium-authenticate:443
# When communicating internally, rPC is going to get a name conflict expecting an external
# facing certificate name (i.e. authenticate-service.local vs *.corp.example.com).
- OVERIDE_CERTIFICATE_NAME=*.corp.beyondperimeter.com
- ROUTES=https://gitlab.corp.beyondperimeter.com=https://gitlab
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
- SIGNING_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
# nginx settings
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=*.corp.beyondperimeter.com

View file

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

View file

@ -16,7 +16,7 @@ spec:
app: pomerium-authenticate
spec:
containers:
- image: pomerium/pomerium:latest
- image: pomerium/pomerium:grpctest
name: pomerium-authenticate
ports:
- containerPort: 443
@ -26,7 +26,7 @@ spec:
- name: SERVICES
value: authenticate
- name: REDIRECT_URL
value: https://sso-auth.corp.beyondperimeter.com/oauth2/callback
value: https://auth.corp.beyondperimeter.com/oauth2/callback
- name: IDP_PROVIDER
value: google
- name: IDP_PROVIDER_URL
@ -62,12 +62,6 @@ spec:
secretKeyRef:
name: certificate-key
key: certificate-key
- name: VIRTUAL_PROTO
value: https
- name: VIRTUAL_HOST
value: sso-auth.corp.beyondperimeter.com
- name: VIRTUAL_PORT
value: "443"
readinessProbe:
httpGet:
path: /ping

View file

@ -0,0 +1,34 @@
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: pomerium-http
namespace: pomerium
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
# nginx.ingress.kubernetes.io/ssl-passthrough: "true"
spec:
tls:
- secretName: pomerium-tls
hosts:
- "*.corp.beyondperimeter.com"
- "auth.corp.beyondperimeter.com"
rules:
- host: "*.corp.beyondperimeter.com"
http:
paths:
- paths:
backend:
serviceName: pomerium-proxy-service
servicePort: https
- host: "auth.corp.beyondperimeter.com"
http:
paths:
- paths:
backend:
serviceName: pomerium-authenticate-service
servicePort: https

View file

@ -12,28 +12,20 @@ spec:
- secretName: pomerium-tls
hosts:
- "*.corp.beyondperimeter.com"
- "sso-auth.corp.beyondperimeter.com"
- "auth.corp.beyondperimeter.com"
rules:
- host: "*.corp.beyondperimeter.com"
http:
paths:
- path: /
- paths:
backend:
serviceName: pomerium-proxy-service
servicePort: 443
- path: /*
backend:
serviceName: pomerium-proxy-service
servicePort: 443
servicePort: https
- host: "sso-auth.corp.beyondperimeter.com"
- host: "auth.corp.beyondperimeter.com"
http:
paths:
- path: /*
- paths:
backend:
serviceName: pomerium-authenticate-service
servicePort: 443
- path: /
backend:
serviceName: pomerium-authenticate-service
servicePort: 443
servicePort: https

View file

@ -16,7 +16,7 @@ spec:
app: pomerium-proxy
spec:
containers:
- image: pomerium/pomerium:latest
- image: pomerium/pomerium:grpctest
name: pomerium-proxy
ports:
- containerPort: 443
@ -24,11 +24,15 @@ spec:
protocol: TCP
env:
- name: ROUTES
value: https://httpbin.corp.beyondperimeter.com=https://httpbin.org
value: https://httpbin.corp.beyondperimeter.com=https://httpbin.org,https://hi.corp.beyondperimeter.com=http://hello-app.pomerium.svc.cluster.local:8080
- name: SERVICES
value: proxy
- name: AUTHENTICATE_SERVICE_URL
value: https://sso-auth.corp.beyondperimeter.com
value: https://auth.corp.beyondperimeter.com
- name: AUTHENTICATE_INTERNAL_URL
value: "pomerium-authenticate-service.pomerium.svc.cluster.local:443"
- name: OVERIDE_CERTIFICATE_NAME
value: "*.corp.beyondperimeter.com"
- name: SHARED_SECRET
valueFrom:
secretKeyRef:
@ -54,12 +58,6 @@ spec:
secretKeyRef:
name: certificate-key
key: certificate-key
- name: VIRTUAL_PROTO
value: https
- name: VIRTUAL_HOST
value: "*.corp.beyondperimeter.com"
- name: VIRTUAL_PORT
value: "443"
readinessProbe:
httpGet:
path: /ping

View file

@ -10,9 +10,9 @@ description: >-
This article describes how to configure pomerium to use a third-party identity service for single-sign-on.
There are a few configuration steps required for identity provider integration. Most providers support [OpenID Connect] which provides a standardized interface for authentication. In this guide we'll cover how to do the following for each identity provider:
There are a few configuration steps required for identity provider integration. Most providers support [OpenID Connect] which provides a standardized interface for IdentityProvider. In this guide we'll cover how to do the following for each identity provider:
1. Establish a **Redirect URL** with the identity provider which is called after authentication.
1. Establish a **Redirect URL** with the identity provider which is called after IdentityProvider.
2. Generate a **Client ID** and **Client Secret**.
3. Configure pomerium to use the **Client ID** and **Client Secret** keys.

View file

@ -52,7 +52,7 @@ Run [./scripts/kubernetes_gke.sh] which will:
1. Provision a new cluster
2. Create authenticate and proxy [deployments](https://cloud.google.com/kubernetes-engine/docs/concepts/deployment).
3. Provision and apply authenticate and proxy [services](https://cloud.google.com/kubernetes-engine/docs/concepts/service).
4. Configure an ingress to do serve TLS between client and load balancer
4. Configure an ingress load balancer.
```bash
sh ./scripts/kubernetes_gke.sh

View file

@ -20,7 +20,7 @@ Place your domain's wild-card TLS certificate next to the compose file. If you d
## Run
Docker-compose will automatically download the latest pomerium release as well as two example containers and an nginx load balancer all in one step.
Docker-compose will automatically download the latest pomerium release as well as two example containers.
```bash
docker-compose up

5
go.mod
View file

@ -3,6 +3,8 @@ module github.com/pomerium/pomerium
require (
github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/mock v1.2.0
github.com/golang/protobuf v1.2.0
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31
github.com/pomerium/go-oidc v2.0.0+incompatible
@ -10,10 +12,11 @@ require (
github.com/rs/zerolog v1.11.0
github.com/stretchr/testify v1.2.2 // indirect
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect
golang.org/x/net v0.0.0-20181220203305-927f97764cc3
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 // indirect
google.golang.org/appengine v1.4.0 // indirect
google.golang.org/grpc v1.18.0
gopkg.in/square/go-jose.v2 v2.2.1
)

22
go.sum
View file

@ -1,9 +1,18 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3 h1:wOysYcIdqv3WnvwqFFzrYCFALPED7qkUGaLXu359GSc=
github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3/go.mod h1:UMqtWQTnOe4byzwe7Zhwh8f8s+36uszN51sJrSIZlTE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
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/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/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
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/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31 h1:bNqUesLWa+RUxQvSaV3//dEFviXdCSvMF9GKDOopFLU=
@ -18,17 +27,30 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
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-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0=
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
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/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA=
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
gopkg.in/square/go-jose.v2 v2.2.1 h1:uRIz/V7RfMsMgGnCp+YybIdstDIz8wc0H283wHQfwic=
gopkg.in/square/go-jose.v2 v2.2.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -3,12 +3,12 @@ version: 0.0.1
apiVersion: v1
appVersion: 0.0.1
home: http://www.pomerium.io/
description: A reverse proxy that provides authentication with Google, Okta, Azure or other providers
description: A reverse proxy that provides IdentityProvider with Google, Okta, Azure or other providers
keywords:
- kubernetes
- oauth
- oauth2
- authentication
- IdentityProvider
- google
- okta
- azure

View file

@ -24,7 +24,7 @@ proxy:
# For any other settings that are optional
# ADDRESS, POMERIUM_DEBUG, CERTIFICATE_FILE, CERTIFICATE_KEY_FILE
# PROXY_ROOT_DOMAIN, COOKIE_DOMAIN, COOKIE_EXPIRE, COOKIE_REFRESH, COOKIE_SECURE, COOKIE_HTTP_ONLY, IDP_SCOPE
# PROXY_ROOT_DOMAIN, COOKIE_DOMAIN, COOKIE_EXPIRE, COOKIE_REFRESH, COOKIE_SECURE, COOKIE_HTTP_ONLY, IDP_SCOPES
# DEFAULT_UPSTREAM_TIMEOUT, PASS_ACCESS_TOKEN, SESSION_VALID_TTL, SESSION_LIFETIME_TTL, GRACE_PERIOD_TTL
extraEnv: {}

View file

@ -38,7 +38,7 @@ type XChaCha20Cipher struct {
aead cipher.AEAD
}
// NewCipher returns a new XChacha20poly1305 cipher.
// NewCipher takes secret key and returns a new XChacha20poly1305 cipher.
func NewCipher(secret []byte) (*XChaCha20Cipher, error) {
aead, err := chacha20poly1305.NewX(secret)
if err != nil {

View file

@ -8,9 +8,11 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/pomerium/pomerium/internal/fileutil"
"google.golang.org/grpc"
)
// Options contains the configurations settings for a TLS http server.
@ -55,7 +57,7 @@ func (opt *Options) applyDefaults() {
// ListenAndServeTLS serves the provided handlers by HTTPS
// using the provided options.
func ListenAndServeTLS(opt *Options, handler http.Handler) error {
func ListenAndServeTLS(opt *Options, httpHandler http.Handler, grpcHandler *grpc.Server) error {
if opt == nil {
opt = defaultOptions
} else {
@ -82,16 +84,21 @@ func ListenAndServeTLS(opt *Options, handler http.Handler) error {
ln = tls.NewListener(ln, config)
var h http.Handler
if grpcHandler == nil {
h = httpHandler
} else {
h = grpcHandlerFunc(grpcHandler, httpHandler)
}
// Set up the main server.
server := &http.Server{
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
// WriteTimeout is set to 0 because it also pertains to
// streaming replies, e.g., the DirServer.Watch interface.
ReadTimeout: 10 * time.Second,
// WriteTimeout is set to 0 for streaming replies
WriteTimeout: 0,
IdleTimeout: 60 * time.Second,
TLSConfig: config,
Handler: handler,
Handler: h,
}
return server.Serve(ln)
@ -130,10 +137,15 @@ func readCertificateFile(certFile, certKeyFile string) (*tls.Certificate, error)
}
// newDefaultTLSConfig creates a new TLS config based on the certificate files given.
// see also:
// See :
// https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_configurations
// https://blog.cloudflare.com/exposing-go-on-the-internet/
// https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices
// https://github.com/golang/go/blob/df91b8044dbe790c69c16058330f545be069cc1f/src/crypto/tls/common.go#L919
func newDefaultTLSConfig(cert *tls.Certificate) (*tls.Config, error) {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
// Prioritize cipher suites sped up by AES-NI (AES-GCM)
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
@ -142,10 +154,29 @@ func newDefaultTLSConfig(cert *tls.Certificate) (*tls.Config, error) {
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
Certificates: []tls.Certificate{*cert},
// Use curves which have assembly implementations
CurvePreferences: []tls.CurveID{
tls.CurveP256,
tls.X25519,
},
Certificates: []tls.Certificate{*cert},
// HTTP/2 must be enabled manually when using http.Serve
NextProtos: []string{"h2"},
}
tlsConfig.BuildNameToCertificate()
return tlsConfig, nil
}
// grpcHandlerFunc splits request serving between gRPC and HTTPS depending on the request type.
// Requires HTTP/2.
func grpcHandlerFunc(rpcServer *grpc.Server, other http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ct := r.Header.Get("Content-Type")
if r.ProtoMajor == 2 && strings.Contains(ct, "application/grpc") {
rpcServer.ServeHTTP(w, r)
} else {
other.ServeHTTP(w, r)
}
})
}

View file

@ -11,6 +11,8 @@ import (
"net/http"
"net/url"
"time"
"github.com/pomerium/pomerium/internal/log"
)
// ErrTokenRevoked signifies a token revokation or expiration error
@ -59,6 +61,7 @@ func Client(method, endpoint, userAgent string, params url.Values, response inte
if err != nil {
return err
}
log.Info().Msgf("%s", respBody)
if resp.StatusCode != http.StatusOK {
switch resp.StatusCode {

View file

@ -10,11 +10,12 @@ import (
)
// Logger is the global logger.
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
var Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()
// SetDebugMode tells the logger to use standard out and pretty print output.
func SetDebugMode() {
Logger = Logger.Output(zerolog.ConsoleWriter{Out: os.Stdout})
// zerolog.SetGlobalLevel(zerolog.InfoLevel)
}
// With creates a child logger with the field added to its context.

View file

@ -0,0 +1,48 @@
package middleware // import "github.com/pomerium/pomerium/internal/middleware"
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// SharedSecretCred is a simple token-based method of mutual authentication.
type SharedSecretCred struct{ sharedSecret string }
// NewSharedSecretCred returns a new instance of shared secret credential middleware for gRPC clients
func NewSharedSecretCred(secret string) *SharedSecretCred {
return &SharedSecretCred{sharedSecret: secret}
}
// GetRequestMetadata sets the value for "authorization" key
func (s SharedSecretCred) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
return map[string]string{"authorization": s.sharedSecret}, nil
}
// RequireTransportSecurity should be true as we want to have it encrypted over the wire.
func (s SharedSecretCred) RequireTransportSecurity() bool { return false }
// ValidateRequest ensures a valid token exists within a request's metadata. If
// the token is missing or invalid, the interceptor blocks execution of the
// handler and returns an error. Otherwise, the interceptor invokes the unary
// handler.
func (s SharedSecretCred) ValidateRequest(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "missing metadata")
}
// The keys within metadata.MD are normalized to lowercase.
// See: https://godoc.org/google.golang.org/grpc/metadata#New
elem, ok := md["authorization"]
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "no auth details supplied")
}
if elem[0] != s.sharedSecret {
return nil, status.Errorf(codes.Unauthenticated, "invalid shared secrets")
}
// Continue execution of handler after ensuring a valid token.
return handler(ctx, req)
}

View file

@ -119,7 +119,7 @@ func ValidateHost(mux map[string]http.Handler) func(next http.Handler) http.Hand
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, ok := mux[r.Host]; !ok {
httputil.ErrorResponse(w, r, "Unknown host to route", http.StatusNotFound)
httputil.ErrorResponse(w, r, "Unknown route", http.StatusNotFound)
return
}
next.ServeHTTP(w, r)

View file

@ -16,15 +16,16 @@ var (
type SessionState struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token"` // https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
IDToken string `json:"id_token"`
RefreshDeadline time.Time `json:"refresh_deadline"`
LifetimeDeadline time.Time `json:"lifetime_deadline"`
ValidDeadline time.Time `json:"valid_deadline"`
GracePeriodStart time.Time `json:"grace_period_start"`
Email string `json:"email"`
User string `json:"user"`
Email string `json:"email"`
User string `json:"user"` // 'sub' in jwt parlance
Groups []string `json:"groups"`
}
// LifetimePeriodExpired returns true if the lifetime has expired

View file

@ -2,11 +2,12 @@ package templates // import "github.com/pomerium/pomerium/internal/templates"
import (
"testing"
"github.com/pomerium/pomerium/internal/testutil"
)
func TestTemplatesCompile(t *testing.T) {
templates := New()
testutil.NotEqual(t, templates, nil)
if templates == nil {
t.Errorf("unexpected nil value %#v", templates)
}
}

View file

@ -0,0 +1,477 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: authenticate.proto
package authenticate
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import timestamp "github.com/golang/protobuf/ptypes/timestamp"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type AuthenticateRequest struct {
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *AuthenticateRequest) Reset() { *m = AuthenticateRequest{} }
func (m *AuthenticateRequest) String() string { return proto.CompactTextString(m) }
func (*AuthenticateRequest) ProtoMessage() {}
func (*AuthenticateRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_authenticate_b52fdd447b0a5778, []int{0}
}
func (m *AuthenticateRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_AuthenticateRequest.Unmarshal(m, b)
}
func (m *AuthenticateRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_AuthenticateRequest.Marshal(b, m, deterministic)
}
func (dst *AuthenticateRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_AuthenticateRequest.Merge(dst, src)
}
func (m *AuthenticateRequest) XXX_Size() int {
return xxx_messageInfo_AuthenticateRequest.Size(m)
}
func (m *AuthenticateRequest) XXX_DiscardUnknown() {
xxx_messageInfo_AuthenticateRequest.DiscardUnknown(m)
}
var xxx_messageInfo_AuthenticateRequest proto.InternalMessageInfo
func (m *AuthenticateRequest) GetCode() string {
if m != nil {
return m.Code
}
return ""
}
type AuthenticateReply struct {
AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
RefreshToken string `protobuf:"bytes,2,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
IdToken string `protobuf:"bytes,3,opt,name=id_token,json=idToken,proto3" json:"id_token,omitempty"`
User string `protobuf:"bytes,4,opt,name=user,proto3" json:"user,omitempty"`
Email string `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"`
Expiry *timestamp.Timestamp `protobuf:"bytes,6,opt,name=expiry,proto3" json:"expiry,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *AuthenticateReply) Reset() { *m = AuthenticateReply{} }
func (m *AuthenticateReply) String() string { return proto.CompactTextString(m) }
func (*AuthenticateReply) ProtoMessage() {}
func (*AuthenticateReply) Descriptor() ([]byte, []int) {
return fileDescriptor_authenticate_b52fdd447b0a5778, []int{1}
}
func (m *AuthenticateReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_AuthenticateReply.Unmarshal(m, b)
}
func (m *AuthenticateReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_AuthenticateReply.Marshal(b, m, deterministic)
}
func (dst *AuthenticateReply) XXX_Merge(src proto.Message) {
xxx_messageInfo_AuthenticateReply.Merge(dst, src)
}
func (m *AuthenticateReply) XXX_Size() int {
return xxx_messageInfo_AuthenticateReply.Size(m)
}
func (m *AuthenticateReply) XXX_DiscardUnknown() {
xxx_messageInfo_AuthenticateReply.DiscardUnknown(m)
}
var xxx_messageInfo_AuthenticateReply proto.InternalMessageInfo
func (m *AuthenticateReply) GetAccessToken() string {
if m != nil {
return m.AccessToken
}
return ""
}
func (m *AuthenticateReply) GetRefreshToken() string {
if m != nil {
return m.RefreshToken
}
return ""
}
func (m *AuthenticateReply) GetIdToken() string {
if m != nil {
return m.IdToken
}
return ""
}
func (m *AuthenticateReply) GetUser() string {
if m != nil {
return m.User
}
return ""
}
func (m *AuthenticateReply) GetEmail() string {
if m != nil {
return m.Email
}
return ""
}
func (m *AuthenticateReply) GetExpiry() *timestamp.Timestamp {
if m != nil {
return m.Expiry
}
return nil
}
type ValidateRequest struct {
IdToken string `protobuf:"bytes,1,opt,name=id_token,json=idToken,proto3" json:"id_token,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ValidateRequest) Reset() { *m = ValidateRequest{} }
func (m *ValidateRequest) String() string { return proto.CompactTextString(m) }
func (*ValidateRequest) ProtoMessage() {}
func (*ValidateRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_authenticate_b52fdd447b0a5778, []int{2}
}
func (m *ValidateRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ValidateRequest.Unmarshal(m, b)
}
func (m *ValidateRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ValidateRequest.Marshal(b, m, deterministic)
}
func (dst *ValidateRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_ValidateRequest.Merge(dst, src)
}
func (m *ValidateRequest) XXX_Size() int {
return xxx_messageInfo_ValidateRequest.Size(m)
}
func (m *ValidateRequest) XXX_DiscardUnknown() {
xxx_messageInfo_ValidateRequest.DiscardUnknown(m)
}
var xxx_messageInfo_ValidateRequest proto.InternalMessageInfo
func (m *ValidateRequest) GetIdToken() string {
if m != nil {
return m.IdToken
}
return ""
}
type ValidateReply struct {
IsValid bool `protobuf:"varint,1,opt,name=is_valid,json=isValid,proto3" json:"is_valid,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ValidateReply) Reset() { *m = ValidateReply{} }
func (m *ValidateReply) String() string { return proto.CompactTextString(m) }
func (*ValidateReply) ProtoMessage() {}
func (*ValidateReply) Descriptor() ([]byte, []int) {
return fileDescriptor_authenticate_b52fdd447b0a5778, []int{3}
}
func (m *ValidateReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ValidateReply.Unmarshal(m, b)
}
func (m *ValidateReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ValidateReply.Marshal(b, m, deterministic)
}
func (dst *ValidateReply) XXX_Merge(src proto.Message) {
xxx_messageInfo_ValidateReply.Merge(dst, src)
}
func (m *ValidateReply) XXX_Size() int {
return xxx_messageInfo_ValidateReply.Size(m)
}
func (m *ValidateReply) XXX_DiscardUnknown() {
xxx_messageInfo_ValidateReply.DiscardUnknown(m)
}
var xxx_messageInfo_ValidateReply proto.InternalMessageInfo
func (m *ValidateReply) GetIsValid() bool {
if m != nil {
return m.IsValid
}
return false
}
type RefreshRequest struct {
RefreshToken string `protobuf:"bytes,1,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *RefreshRequest) Reset() { *m = RefreshRequest{} }
func (m *RefreshRequest) String() string { return proto.CompactTextString(m) }
func (*RefreshRequest) ProtoMessage() {}
func (*RefreshRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_authenticate_b52fdd447b0a5778, []int{4}
}
func (m *RefreshRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_RefreshRequest.Unmarshal(m, b)
}
func (m *RefreshRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_RefreshRequest.Marshal(b, m, deterministic)
}
func (dst *RefreshRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_RefreshRequest.Merge(dst, src)
}
func (m *RefreshRequest) XXX_Size() int {
return xxx_messageInfo_RefreshRequest.Size(m)
}
func (m *RefreshRequest) XXX_DiscardUnknown() {
xxx_messageInfo_RefreshRequest.DiscardUnknown(m)
}
var xxx_messageInfo_RefreshRequest proto.InternalMessageInfo
func (m *RefreshRequest) GetRefreshToken() string {
if m != nil {
return m.RefreshToken
}
return ""
}
type RefreshReply struct {
AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
Expiry *timestamp.Timestamp `protobuf:"bytes,2,opt,name=expiry,proto3" json:"expiry,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *RefreshReply) Reset() { *m = RefreshReply{} }
func (m *RefreshReply) String() string { return proto.CompactTextString(m) }
func (*RefreshReply) ProtoMessage() {}
func (*RefreshReply) Descriptor() ([]byte, []int) {
return fileDescriptor_authenticate_b52fdd447b0a5778, []int{5}
}
func (m *RefreshReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_RefreshReply.Unmarshal(m, b)
}
func (m *RefreshReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_RefreshReply.Marshal(b, m, deterministic)
}
func (dst *RefreshReply) XXX_Merge(src proto.Message) {
xxx_messageInfo_RefreshReply.Merge(dst, src)
}
func (m *RefreshReply) XXX_Size() int {
return xxx_messageInfo_RefreshReply.Size(m)
}
func (m *RefreshReply) XXX_DiscardUnknown() {
xxx_messageInfo_RefreshReply.DiscardUnknown(m)
}
var xxx_messageInfo_RefreshReply proto.InternalMessageInfo
func (m *RefreshReply) GetAccessToken() string {
if m != nil {
return m.AccessToken
}
return ""
}
func (m *RefreshReply) GetExpiry() *timestamp.Timestamp {
if m != nil {
return m.Expiry
}
return nil
}
func init() {
proto.RegisterType((*AuthenticateRequest)(nil), "authenticate.AuthenticateRequest")
proto.RegisterType((*AuthenticateReply)(nil), "authenticate.AuthenticateReply")
proto.RegisterType((*ValidateRequest)(nil), "authenticate.ValidateRequest")
proto.RegisterType((*ValidateReply)(nil), "authenticate.ValidateReply")
proto.RegisterType((*RefreshRequest)(nil), "authenticate.RefreshRequest")
proto.RegisterType((*RefreshReply)(nil), "authenticate.RefreshReply")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// AuthenticatorClient is the client API for Authenticator service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type AuthenticatorClient interface {
Authenticate(ctx context.Context, in *AuthenticateRequest, opts ...grpc.CallOption) (*AuthenticateReply, error)
Validate(ctx context.Context, in *ValidateRequest, opts ...grpc.CallOption) (*ValidateReply, error)
Refresh(ctx context.Context, in *RefreshRequest, opts ...grpc.CallOption) (*RefreshReply, error)
}
type authenticatorClient struct {
cc *grpc.ClientConn
}
func NewAuthenticatorClient(cc *grpc.ClientConn) AuthenticatorClient {
return &authenticatorClient{cc}
}
func (c *authenticatorClient) Authenticate(ctx context.Context, in *AuthenticateRequest, opts ...grpc.CallOption) (*AuthenticateReply, error) {
out := new(AuthenticateReply)
err := c.cc.Invoke(ctx, "/authenticate.Authenticator/Authenticate", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authenticatorClient) Validate(ctx context.Context, in *ValidateRequest, opts ...grpc.CallOption) (*ValidateReply, error) {
out := new(ValidateReply)
err := c.cc.Invoke(ctx, "/authenticate.Authenticator/Validate", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authenticatorClient) Refresh(ctx context.Context, in *RefreshRequest, opts ...grpc.CallOption) (*RefreshReply, error) {
out := new(RefreshReply)
err := c.cc.Invoke(ctx, "/authenticate.Authenticator/Refresh", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// AuthenticatorServer is the server API for Authenticator service.
type AuthenticatorServer interface {
Authenticate(context.Context, *AuthenticateRequest) (*AuthenticateReply, error)
Validate(context.Context, *ValidateRequest) (*ValidateReply, error)
Refresh(context.Context, *RefreshRequest) (*RefreshReply, error)
}
func RegisterAuthenticatorServer(s *grpc.Server, srv AuthenticatorServer) {
s.RegisterService(&_Authenticator_serviceDesc, srv)
}
func _Authenticator_Authenticate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AuthenticateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthenticatorServer).Authenticate(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/authenticate.Authenticator/Authenticate",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthenticatorServer).Authenticate(ctx, req.(*AuthenticateRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Authenticator_Validate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ValidateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthenticatorServer).Validate(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/authenticate.Authenticator/Validate",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthenticatorServer).Validate(ctx, req.(*ValidateRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Authenticator_Refresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RefreshRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthenticatorServer).Refresh(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/authenticate.Authenticator/Refresh",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthenticatorServer).Refresh(ctx, req.(*RefreshRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Authenticator_serviceDesc = grpc.ServiceDesc{
ServiceName: "authenticate.Authenticator",
HandlerType: (*AuthenticatorServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Authenticate",
Handler: _Authenticator_Authenticate_Handler,
},
{
MethodName: "Validate",
Handler: _Authenticator_Validate_Handler,
},
{
MethodName: "Refresh",
Handler: _Authenticator_Refresh_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "authenticate.proto",
}
func init() { proto.RegisterFile("authenticate.proto", fileDescriptor_authenticate_b52fdd447b0a5778) }
var fileDescriptor_authenticate_b52fdd447b0a5778 = []byte{
// 364 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0x4f, 0x4f, 0xea, 0x40,
0x14, 0xc5, 0x5f, 0x79, 0xfc, 0x7b, 0x97, 0xf2, 0x5e, 0xde, 0xd5, 0x45, 0xad, 0x1a, 0xa0, 0x6e,
0xd0, 0x98, 0x92, 0x60, 0xfc, 0x00, 0x2e, 0x4c, 0x5c, 0x37, 0xc4, 0x2d, 0x29, 0xed, 0x05, 0x26,
0x16, 0xa6, 0x76, 0xa6, 0x46, 0xbe, 0xa7, 0x9f, 0xc5, 0xb5, 0xe9, 0x4c, 0x2b, 0xad, 0x88, 0x61,
0xd7, 0x9e, 0xf3, 0x9b, 0x9b, 0x7b, 0xce, 0x0c, 0xa0, 0x9f, 0xca, 0x25, 0xad, 0x25, 0x0b, 0x7c,
0x49, 0x6e, 0x9c, 0x70, 0xc9, 0xd1, 0x2c, 0x6b, 0x76, 0x6f, 0xc1, 0xf9, 0x22, 0xa2, 0x91, 0xf2,
0x66, 0xe9, 0x7c, 0x24, 0xd9, 0x8a, 0x84, 0xf4, 0x57, 0xb1, 0xc6, 0x9d, 0x4b, 0x38, 0xba, 0x2b,
0x1d, 0xf0, 0xe8, 0x39, 0x25, 0x21, 0x11, 0xa1, 0x1e, 0xf0, 0x90, 0x2c, 0xa3, 0x6f, 0x0c, 0xff,
0x78, 0xea, 0xdb, 0x79, 0x33, 0xe0, 0x7f, 0x95, 0x8d, 0xa3, 0x0d, 0x0e, 0xc0, 0xf4, 0x83, 0x80,
0x84, 0x98, 0x4a, 0xfe, 0x44, 0xeb, 0xfc, 0x44, 0x47, 0x6b, 0x93, 0x4c, 0xc2, 0x0b, 0xe8, 0x26,
0x34, 0x4f, 0x48, 0x2c, 0x73, 0xa6, 0xa6, 0x18, 0x33, 0x17, 0x35, 0x74, 0x02, 0x6d, 0x16, 0xe6,
0xfe, 0x6f, 0xe5, 0xb7, 0x58, 0xa8, 0x2d, 0x84, 0x7a, 0x2a, 0x28, 0xb1, 0xea, 0x7a, 0x99, 0xec,
0x1b, 0x8f, 0xa1, 0x41, 0x2b, 0x9f, 0x45, 0x56, 0x43, 0x89, 0xfa, 0x07, 0xc7, 0xd0, 0xa4, 0xd7,
0x98, 0x25, 0x1b, 0xab, 0xd9, 0x37, 0x86, 0x9d, 0xb1, 0xed, 0xea, 0xfc, 0x6e, 0x91, 0xdf, 0x9d,
0x14, 0xf9, 0xbd, 0x9c, 0x74, 0xae, 0xe1, 0xdf, 0xa3, 0x1f, 0xb1, 0xb0, 0x94, 0xbe, 0xbc, 0x8b,
0x51, 0xd9, 0xc5, 0xb9, 0x82, 0xee, 0x96, 0xce, 0xf2, 0x67, 0xac, 0x98, 0xbe, 0x64, 0x9a, 0x62,
0xdb, 0x5e, 0x8b, 0x09, 0x85, 0x38, 0xb7, 0xf0, 0xd7, 0xd3, 0x11, 0x8b, 0xc1, 0x3b, 0x4d, 0x18,
0xbb, 0x4d, 0x38, 0x04, 0xe6, 0xe7, 0xb1, 0x03, 0x1b, 0xde, 0xe6, 0xae, 0x1d, 0x9a, 0x7b, 0xfc,
0x6e, 0x40, 0xb7, 0x74, 0x9d, 0x3c, 0xc1, 0x09, 0x98, 0xe5, 0xfb, 0xc5, 0x81, 0x5b, 0x79, 0x5f,
0xdf, 0xbc, 0x13, 0xbb, 0xf7, 0x13, 0x12, 0x47, 0x1b, 0xe7, 0x17, 0x3e, 0x40, 0xbb, 0x68, 0x0c,
0xcf, 0xab, 0xf8, 0x97, 0xde, 0xed, 0xd3, 0x7d, 0xb6, 0x9e, 0x74, 0x0f, 0xad, 0xbc, 0x18, 0x3c,
0xab, 0x92, 0xd5, 0x9a, 0x6d, 0x7b, 0x8f, 0xab, 0xc6, 0xcc, 0x9a, 0xaa, 0x94, 0x9b, 0x8f, 0x00,
0x00, 0x00, 0xff, 0xff, 0x76, 0x32, 0xe7, 0x1e, 0x3e, 0x03, 0x00, 0x00,
}

View file

@ -0,0 +1,32 @@
syntax = "proto3";
import "google/protobuf/timestamp.proto";
package authenticate;
service Authenticator {
rpc Authenticate(AuthenticateRequest) returns (AuthenticateReply) {}
rpc Validate(ValidateRequest) returns (ValidateReply) {}
rpc Refresh(RefreshRequest) returns (RefreshReply) {}
}
message AuthenticateRequest { string code = 1; }
message AuthenticateReply {
string access_token = 1;
string refresh_token = 2;
string id_token = 3;
string user = 4;
string email = 5;
google.protobuf.Timestamp expiry = 6;
}
message ValidateRequest { string id_token = 1; }
message ValidateReply { bool is_valid = 1; }
message RefreshRequest { string refresh_token = 1; }
message RefreshReply {
string access_token = 1;
google.protobuf.Timestamp expiry = 2;
}

View file

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

View file

@ -0,0 +1,97 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/pomerium/pomerium/proto/authenticate (interfaces: AuthenticatorClient)
// Package mock_authenticate is a generated GoMock package.
package mock_authenticate
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
authenticate "github.com/pomerium/pomerium/proto/authenticate"
grpc "google.golang.org/grpc"
)
// MockAuthenticatorClient is a mock of AuthenticatorClient interface
type MockAuthenticatorClient struct {
ctrl *gomock.Controller
recorder *MockAuthenticatorClientMockRecorder
}
// MockAuthenticatorClientMockRecorder is the mock recorder for MockAuthenticatorClient
type MockAuthenticatorClientMockRecorder struct {
mock *MockAuthenticatorClient
}
// NewMockAuthenticatorClient creates a new mock instance
func NewMockAuthenticatorClient(ctrl *gomock.Controller) *MockAuthenticatorClient {
mock := &MockAuthenticatorClient{ctrl: ctrl}
mock.recorder = &MockAuthenticatorClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockAuthenticatorClient) EXPECT() *MockAuthenticatorClientMockRecorder {
return m.recorder
}
// Authenticate mocks base method
func (m *MockAuthenticatorClient) Authenticate(arg0 context.Context, arg1 *authenticate.AuthenticateRequest, arg2 ...grpc.CallOption) (*authenticate.AuthenticateReply, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Authenticate", varargs...)
ret0, _ := ret[0].(*authenticate.AuthenticateReply)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Authenticate indicates an expected call of Authenticate
func (mr *MockAuthenticatorClientMockRecorder) Authenticate(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockAuthenticatorClient)(nil).Authenticate), varargs...)
}
// Refresh mocks base method
func (m *MockAuthenticatorClient) Refresh(arg0 context.Context, arg1 *authenticate.RefreshRequest, arg2 ...grpc.CallOption) (*authenticate.RefreshReply, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Refresh", varargs...)
ret0, _ := ret[0].(*authenticate.RefreshReply)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh
func (mr *MockAuthenticatorClientMockRecorder) Refresh(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockAuthenticatorClient)(nil).Refresh), varargs...)
}
// Validate mocks base method
func (m *MockAuthenticatorClient) Validate(arg0 context.Context, arg1 *authenticate.ValidateRequest, arg2 ...grpc.CallOption) (*authenticate.ValidateReply, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Validate", varargs...)
ret0, _ := ret[0].(*authenticate.ValidateReply)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Validate indicates an expected call of Validate
func (mr *MockAuthenticatorClientMockRecorder) Validate(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockAuthenticatorClient)(nil).Validate), varargs...)
}

View file

@ -1,308 +0,0 @@
package authenticator // import "github.com/pomerium/pomerium/proxy/authenticator"
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version"
)
var defaultHTTPClient = &http.Client{
Timeout: time.Second * 5,
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: 2 * time.Second,
}).Dial,
TLSHandshakeTimeout: 2 * time.Second,
},
}
// Errors
var (
ErrMissingRefreshToken = errors.New("missing refresh token")
ErrAuthProviderUnavailable = errors.New("auth provider unavailable")
)
// AuthenticateClient holds the data associated with the AuthenticateClients
// necessary to implement a AuthenticateClient interface.
type AuthenticateClient struct {
AuthenticateServiceURL *url.URL
SharedKey string
SignInURL *url.URL
SignOutURL *url.URL
RedeemURL *url.URL
RefreshURL *url.URL
ProfileURL *url.URL
ValidateURL *url.URL
SessionValidTTL time.Duration
SessionLifetimeTTL time.Duration
GracePeriodTTL time.Duration
}
// NewClient instantiates a new AuthenticateClient with provider data
func NewClient(uri *url.URL, sharedKey string, sessionValid, sessionLifetime, gracePeriod time.Duration) *AuthenticateClient {
return &AuthenticateClient{
AuthenticateServiceURL: uri,
SharedKey: sharedKey,
SignInURL: uri.ResolveReference(&url.URL{Path: "/sign_in"}),
SignOutURL: uri.ResolveReference(&url.URL{Path: "/sign_out"}),
RedeemURL: uri.ResolveReference(&url.URL{Path: "/redeem"}),
RefreshURL: uri.ResolveReference(&url.URL{Path: "/refresh"}),
ValidateURL: uri.ResolveReference(&url.URL{Path: "/validate"}),
ProfileURL: uri.ResolveReference(&url.URL{Path: "/profile"}),
SessionValidTTL: sessionValid,
SessionLifetimeTTL: sessionLifetime,
GracePeriodTTL: gracePeriod,
}
}
func (p *AuthenticateClient) newRequest(method, url string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", version.UserAgent())
req.Header.Set("Accept", "application/json")
req.Host = p.AuthenticateServiceURL.Host
return req, nil
}
func isProviderUnavailable(statusCode int) bool {
return statusCode == http.StatusTooManyRequests || statusCode == http.StatusServiceUnavailable
}
func extendDeadline(ttl time.Duration) time.Time {
return time.Now().Add(ttl).Truncate(time.Second)
}
func (p *AuthenticateClient) withinGracePeriod(s *sessions.SessionState) bool {
if s.GracePeriodStart.IsZero() {
s.GracePeriodStart = time.Now()
}
return s.GracePeriodStart.Add(p.GracePeriodTTL).After(time.Now())
}
// Redeem takes a redirectURL and code and redeems the SessionState
func (p *AuthenticateClient) Redeem(redirectURL, code string) (*sessions.SessionState, error) {
if code == "" {
return nil, errors.New("missing code")
}
params := url.Values{}
params.Add("shared_secret", p.SharedKey)
params.Add("code", code)
req, err := p.newRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := defaultHTTPClient.Do(req)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
if isProviderUnavailable(resp.StatusCode) {
return nil, ErrAuthProviderUnavailable
}
return nil, fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemURL.String(), body)
}
var jsonResponse struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
Email string `json:"email"`
}
err = json.Unmarshal(body, &jsonResponse)
if err != nil {
return nil, err
}
user := strings.Split(jsonResponse.Email, "@")[0]
return &sessions.SessionState{
AccessToken: jsonResponse.AccessToken,
RefreshToken: jsonResponse.RefreshToken,
IDToken: jsonResponse.IDToken,
RefreshDeadline: extendDeadline(time.Duration(jsonResponse.ExpiresIn) * time.Second),
LifetimeDeadline: extendDeadline(p.SessionLifetimeTTL),
ValidDeadline: extendDeadline(p.SessionValidTTL),
Email: jsonResponse.Email,
User: user,
}, nil
}
// RefreshSession refreshes the current session
func (p *AuthenticateClient) RefreshSession(s *sessions.SessionState) (bool, error) {
if s.RefreshToken == "" {
return false, ErrMissingRefreshToken
}
newToken, duration, err := p.redeemRefreshToken(s.RefreshToken)
if err != nil {
// When we detect that the auth provider is not explicitly denying
// authentication, and is merely unavailable, we refresh and continue
// as normal during the "grace period"
if err == ErrAuthProviderUnavailable && p.withinGracePeriod(s) {
s.RefreshDeadline = extendDeadline(p.SessionValidTTL)
return true, nil
}
return false, err
}
s.AccessToken = newToken
s.RefreshDeadline = extendDeadline(duration)
s.GracePeriodStart = time.Time{}
log.Info().Str("user", s.Email).Msg("proxy/authenticator.RefreshSession")
return true, nil
}
func (p *AuthenticateClient) redeemRefreshToken(refreshToken string) (token string, expires time.Duration, err error) {
params := url.Values{}
params.Add("shared_secret", p.SharedKey)
params.Add("refresh_token", refreshToken)
var req *http.Request
req, err = p.newRequest("POST", p.RefreshURL.String(), bytes.NewBufferString(params.Encode()))
if err != nil {
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := defaultHTTPClient.Do(req)
if err != nil {
return
}
var body []byte
body, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return
}
if resp.StatusCode != http.StatusCreated {
if isProviderUnavailable(resp.StatusCode) {
err = ErrAuthProviderUnavailable
} else {
err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RefreshURL.String(), body)
}
return
}
var data struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
}
err = json.Unmarshal(body, &data)
if err != nil {
return
}
token = data.AccessToken
expires = time.Duration(data.ExpiresIn) * time.Second
return
}
// ValidateSessionState validates the current sessions state
func (p *AuthenticateClient) ValidateSessionState(s *sessions.SessionState) bool {
// we validate the user's access token is valid
params := url.Values{}
params.Add("shared_secret", p.SharedKey)
req, err := p.newRequest("GET", fmt.Sprintf("%s?%s", p.ValidateURL.String(), params.Encode()), nil)
if err != nil {
log.Info().Err(err).Str("user", s.Email).Msg("proxy/authenticator: error validating session state")
return false
}
req.Header.Set("X-Client-Secret", p.SharedKey)
req.Header.Set("X-Access-Token", s.AccessToken)
req.Header.Set("X-Id-Token", s.IDToken)
resp, err := defaultHTTPClient.Do(req)
if err != nil {
log.Info().Err(err).Str("user", s.Email).Msg("proxy/authenticator: error validating access token")
return false
}
if resp.StatusCode != http.StatusOK {
// When we detect that the auth provider is not explicitly denying
// authentication, and is merely unavailable, we validate and continue
// as normal during the "grace period"
if isProviderUnavailable(resp.StatusCode) && p.withinGracePeriod(s) {
s.ValidDeadline = extendDeadline(p.SessionValidTTL)
return true
}
log.Info().Str("user", s.Email).Int("status-code", resp.StatusCode).Msg("proxy/authenticator: bad status code")
return false
}
s.ValidDeadline = extendDeadline(p.SessionValidTTL)
s.GracePeriodStart = time.Time{}
return true
}
// signRedirectURL signs the redirect url string, given a timestamp, and returns it
func (p *AuthenticateClient) signRedirectURL(rawRedirect string, timestamp time.Time) string {
h := hmac.New(sha256.New, []byte(p.SharedKey))
h.Write([]byte(rawRedirect))
h.Write([]byte(fmt.Sprint(timestamp.Unix())))
return base64.URLEncoding.EncodeToString(h.Sum(nil))
}
// GetSignInURL with typical oauth parameters
func (p *AuthenticateClient) GetSignInURL(redirectURL *url.URL, state string) *url.URL {
a := *p.SignInURL
now := time.Now()
rawRedirect := redirectURL.String()
params, _ := url.ParseQuery(a.RawQuery)
params.Set("redirect_uri", rawRedirect)
params.Set("shared_secret", p.SharedKey)
params.Set("response_type", "code")
params.Add("state", state)
params.Set("ts", fmt.Sprint(now.Unix()))
params.Set("sig", p.signRedirectURL(rawRedirect, now))
a.RawQuery = params.Encode()
return &a
}
// GetSignOutURL creates and returns the sign out URL, given a redirectURL
func (p *AuthenticateClient) GetSignOutURL(redirectURL *url.URL) *url.URL {
a := *p.SignOutURL
now := time.Now()
rawRedirect := redirectURL.String()
params, _ := url.ParseQuery(a.RawQuery)
params.Add("redirect_uri", rawRedirect)
params.Set("ts", fmt.Sprint(now.Unix()))
params.Set("sig", p.signRedirectURL(rawRedirect, now))
a.RawQuery = params.Encode()
return &a
}

80
proxy/grpc.go Normal file
View file

@ -0,0 +1,80 @@
package proxy // import "github.com/pomerium/pomerium/proxy"
import (
"context"
"errors"
"time"
"github.com/golang/protobuf/ptypes"
"github.com/pomerium/pomerium/internal/sessions"
pb "github.com/pomerium/pomerium/proto/authenticate"
)
// AuthenticateRedeem makes an RPC call to the authenticate service to creates a session state
// from an encrypted code provided as a result of an oauth2 callback process.
func (p *Proxy) AuthenticateRedeem(code string) (*sessions.SessionState, error) {
if code == "" {
return nil, errors.New("missing code")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := p.AuthenticatorClient.Authenticate(ctx, &pb.AuthenticateRequest{Code: code})
if err != nil {
return nil, err
}
expiry, err := ptypes.Timestamp(r.Expiry)
if err != nil {
return nil, err
}
return &sessions.SessionState{
AccessToken: r.AccessToken,
RefreshToken: r.RefreshToken,
IDToken: r.IdToken,
User: r.User,
Email: r.Email,
RefreshDeadline: (expiry).Truncate(time.Second),
LifetimeDeadline: extendDeadline(p.CookieLifetimeTTL),
ValidDeadline: extendDeadline(p.CookieExpire),
}, nil
}
// AuthenticateRefresh makes an RPC call to the authenticate service to attempt to refresh the
// user's session. Requires a valid refresh token. Will return an error if the identity provider
// has revoked the session or if the refresh token is no longer valid in this context.
func (p *Proxy) AuthenticateRefresh(refreshToken string) (string, time.Time, error) {
if refreshToken == "" {
return "", time.Time{}, errors.New("missing refresh token")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := p.AuthenticatorClient.Refresh(ctx, &pb.RefreshRequest{RefreshToken: refreshToken})
if err != nil {
return "", time.Time{}, err
}
expiry, err := ptypes.Timestamp(r.Expiry)
if err != nil {
return "", time.Time{}, err
}
return r.AccessToken, expiry, nil
}
// AuthenticateValidate makes an RPC call to the authenticate service to validate the JWT id token;
// does NOT do nonce or revokation validation.
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func (p *Proxy) AuthenticateValidate(idToken string) (bool, error) {
if idToken == "" {
return false, errors.New("missing id token")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := p.AuthenticatorClient.Validate(ctx, &pb.ValidateRequest{IdToken: idToken})
if err != nil {
return false, err
}
return r.IsValid, nil
}
func extendDeadline(ttl time.Duration) time.Time {
return time.Now().Add(ttl).Truncate(time.Second)
}

204
proxy/grpc_test.go Normal file
View file

@ -0,0 +1,204 @@
package proxy
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/golang/mock/gomock"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
pb "github.com/pomerium/pomerium/proto/authenticate"
mock "github.com/pomerium/pomerium/proto/authenticate/mock_authenticate"
)
var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
// rpcMsg implements the gomock.Matcher interface
type rpcMsg struct {
msg proto.Message
}
func (r *rpcMsg) Matches(msg interface{}) bool {
m, ok := msg.(proto.Message)
if !ok {
return false
}
return proto.Equal(m, r.msg)
}
func (r *rpcMsg) String() string {
return fmt.Sprintf("is %s", r.msg)
}
func TestProxy_AuthenticateRedeem(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockAuthenticateClient := mock.NewMockAuthenticatorClient(ctrl)
req := &pb.AuthenticateRequest{Code: "unit_test"}
mockExpire, err := ptypes.TimestampProto(fixedDate)
if err != nil {
t.Fatalf("%v failed converting timestamp", err)
}
mockAuthenticateClient.EXPECT().Authenticate(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.AuthenticateReply{
AccessToken: "mocked access token",
RefreshToken: "mocked refresh token",
IdToken: "mocked id token",
User: "user1",
Email: "test@email.com",
Expiry: mockExpire,
}, nil)
p := &Proxy{AuthenticatorClient: mockAuthenticateClient}
tests := []struct {
name string
idToken string
want *sessions.SessionState
wantErr bool
}{
{"good", "unit_test", &sessions.SessionState{
AccessToken: "mocked access token",
RefreshToken: "mocked refresh token",
IDToken: "mocked id token",
User: "user1",
Email: "test@email.com",
RefreshDeadline: (fixedDate).Truncate(time.Second),
LifetimeDeadline: extendDeadline(p.CookieLifetimeTTL),
ValidDeadline: extendDeadline(p.CookieExpire),
}, false},
{"empty code", "", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := p.AuthenticateRedeem(tt.idToken)
if (err != nil) != tt.wantErr {
t.Errorf("Proxy.AuthenticateValidate() error = %v,\n wantErr %v", err, tt.wantErr)
return
}
if got != nil {
if got.AccessToken != "mocked access token" {
t.Errorf("authenticate: invalid access token")
}
if got.RefreshToken != "mocked refresh token" {
t.Errorf("authenticate: invalid refresh token")
}
if got.IDToken != "mocked id token" {
t.Errorf("authenticate: invalid id token")
}
if got.User != "user1" {
t.Errorf("authenticate: invalid user")
}
if got.Email != "test@email.com" {
t.Errorf("authenticate: invalid email")
}
}
})
}
}
func TestProxy_AuthenticateValidate(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockAuthenticateClient := mock.NewMockAuthenticatorClient(ctrl)
req := &pb.ValidateRequest{IdToken: "unit_test"}
mockAuthenticateClient.EXPECT().Validate(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.ValidateReply{IsValid: false}, nil)
p := &Proxy{AuthenticatorClient: mockAuthenticateClient}
tests := []struct {
name string
idToken string
want bool
wantErr bool
}{
{"good", "unit_test", false, false},
{"empty id token", "", false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := p.AuthenticateValidate(tt.idToken)
if (err != nil) != tt.wantErr {
t.Errorf("Proxy.AuthenticateValidate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Proxy.AuthenticateValidate() = %v, want %v", got, tt.want)
}
})
}
}
func TestProxy_AuthenticateRefresh(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRefreshClient := mock.NewMockAuthenticatorClient(ctrl)
req := &pb.RefreshRequest{RefreshToken: "unit_test"}
mockExpire, err := ptypes.TimestampProto(fixedDate)
if err != nil {
t.Fatalf("%v failed converting timestamp", err)
}
mockRefreshClient.EXPECT().Refresh(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.RefreshReply{
AccessToken: "mocked access token",
Expiry: mockExpire,
}, nil).AnyTimes()
tests := []struct {
name string
refreshToken string
wantAT string
wantExp time.Time
wantErr bool
}{
{"good", "unit_test", "mocked access token", fixedDate, false},
{"missing refresh", "", "", time.Time{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Proxy{AuthenticatorClient: mockRefreshClient}
got, gotExp, err := p.AuthenticateRefresh(tt.refreshToken)
if (err != nil) != tt.wantErr {
t.Errorf("Proxy.AuthenticateRefresh() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.wantAT {
t.Errorf("Proxy.AuthenticateRefresh() got = %v, want %v", got, tt.wantAT)
}
if !reflect.DeepEqual(gotExp, tt.wantExp) {
t.Errorf("Proxy.AuthenticateRefresh() gotExp = %v, want %v", gotExp, tt.wantExp)
}
})
}
}
func Test_extendDeadline(t *testing.T) {
tests := []struct {
name string
ttl time.Duration
want time.Time
}{
{"good", time.Second, time.Now().Add(time.Second).Truncate(time.Second)},
{"test nanoseconds truncated", 500 * time.Nanosecond, time.Now().Truncate(time.Second)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := extendDeadline(tt.ttl); !reflect.DeepEqual(got, tt.want) {
t.Errorf("extendDeadline() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -1,6 +1,9 @@
package proxy // import "github.com/pomerium/pomerium/proxy"
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net/http"
@ -17,7 +20,7 @@ import (
)
var (
//ErrUserNotAuthorized is set when user is not authorized to access a resource
// ErrUserNotAuthorized is set when user is not authorized to access a resource
ErrUserNotAuthorized = errors.New("user not authorized")
)
@ -27,6 +30,12 @@ var securityHeaders = map[string]string{
"X-XSS-Protection": "1; mode=block",
}
// StateParameter holds the redirect id along with the session id.
type StateParameter struct {
SessionID string `json:"session_id"`
RedirectURI string `json:"redirect_uri"`
}
// Handler returns a http handler for an Proxy
func (p *Proxy) Handler() http.Handler {
// routes
@ -61,6 +70,8 @@ func (p *Proxy) Handler() http.Handler {
c = c.Append(middleware.RefererHandler("referer"))
c = c.Append(middleware.RequestIDHandler("req_id", "Request-Id"))
c = c.Append(middleware.ValidateHost(p.mux))
// serve the middleware and mux
h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mux.ServeHTTP(w, r)
}))
@ -74,7 +85,7 @@ func (p *Proxy) RobotsTxt(w http.ResponseWriter, _ *http.Request) {
}
// Favicon will proxy the request as usual if the user is already authenticated but responds
// with a 404 otherwise, to avoid spurious and confusing authentication attempts when a browser
// with a 404 otherwise, to avoid spurious and confusing IdentityProvider attempts when a browser
// automatically requests the favicon on an error page.
func (p *Proxy) Favicon(w http.ResponseWriter, r *http.Request) {
err := p.Authenticate(w, r)
@ -93,15 +104,13 @@ func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
Host: r.Host,
Path: "/",
}
fullURL := p.authenticateClient.GetSignOutURL(redirectURL)
fullURL := p.GetSignOutURL(p.AuthenticateURL, redirectURL)
http.Redirect(w, r, fullURL.String(), http.StatusFound)
}
// OAuthStart begins the authentication flow, encrypting the redirect url
// OAuthStart begins the IdentityProvider 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) {
// The proxy redirects to the authenticator, and provides it with redirectURI (which points
// back to the sso proxy).
requestURI := r.URL.String()
callbackURL := p.GetRedirectURL(r.Host)
@ -129,19 +138,20 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) {
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return
}
signinURL := p.authenticateClient.GetSignInURL(callbackURL, encryptedState)
log.FromRequest(r).Info().Msg("redirecting to begin auth flow")
signinURL := p.GetSignInURL(p.AuthenticateURL, callbackURL, encryptedState)
log.FromRequest(r).Info().
Str("SigninURL", signinURL.String()).
Msg("redirecting to begin auth flow")
// redirect the user to the IdentityProvider provider along with the encrypted state which
// contains a redirect uri pointing back to the proxy
http.Redirect(w, r, signinURL.String(), http.StatusFound)
}
// OAuthCallback validates the cookie sent back from the provider, then validates he user
// information, and if authorized, redirects the user back to the original application.
// OAuthCallback validates the cookie sent back from the authenticate service. This function will
// contain an error, or it will contain a `code`; the code can be used to fetch an access token, and
// other metadata, from the authenticator.
// finish the oauth cycle
func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
// We receive the callback from the SSO Authenticator. This request will either contain an
// error, or it will contain a `code`; the code can be used to fetch an access token, and
// other metadata, from the authenticator.
// finish the oauth cycle
err := r.ParseForm()
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("failed parsing request form")
@ -153,9 +163,8 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
httputil.ErrorResponse(w, r, errorString, http.StatusForbidden)
return
}
// We begin the process of redeeming the code for an access token.
session, err := p.redeemCode(r.Host, r.Form.Get("code"))
session, err := p.AuthenticateRedeem(r.Form.Get("code"))
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("error redeeming authorization code")
httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError)
@ -208,6 +217,14 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
return
}
log.FromRequest(r).Info().
Str("code", r.Form.Get("code")).
Str("state", r.Form.Get("state")).
Str("RefreshToken", session.RefreshToken).
Str("session", session.AccessToken).
Str("RedirectURI", stateParameter.RedirectURI).
Msg("session")
// This is the redirect back to the original requested application
http.Redirect(w, r, stateParameter.RedirectURI, http.StatusFound)
}
@ -222,16 +239,14 @@ func (p *Proxy) AuthenticateOnly(w http.ResponseWriter, r *http.Request) {
}
// Proxy authenticates a request, either proxying the request if it is authenticated,
// or starting the authentication process if not.
// or starting the IdentityProvider service for validation if not.
func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
// Attempts to validate the user and their cookie.
err := p.Authenticate(w, r)
// If the authentication is not successful we proceed to start the OAuth Flow with
// If the IdentityProvider is not successful we proceed to start the OAuth Flow with
// OAuthStart. If successful, we proceed to proxy to the configured upstream.
if err != nil {
switch err {
case ErrUserNotAuthorized:
//todo(bdd) : custom forbidden page with details and troubleshooting info
log.FromRequest(r).Debug().Err(err).Msg("proxy: user access forbidden")
httputil.ErrorResponse(w, r, "You don't have access", http.StatusForbidden)
return
@ -245,6 +260,9 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
return
}
}
// ! ! !
// todo(bdd): ! Authorization checks will go here !
// ! ! !
// We have validated the users request and now proxy their request to the provided upstream.
route, ok := p.router(r)
@ -270,29 +288,38 @@ func (p *Proxy) Authenticate(w http.ResponseWriter, r *http.Request) (err error)
if err != nil {
return err
}
if session.LifetimePeriodExpired() {
log.FromRequest(r).Info().Msg("proxy.Authenticate: lifetime expired, restarting")
return sessions.ErrLifetimeExpired
} else if session.RefreshPeriodExpired() {
ok, err := p.authenticateClient.RefreshSession(session)
}
if session.RefreshPeriodExpired() {
// AccessToken's usually expire after 60 or so minutes. If offline_access scope is set, a
// refresh token (which doesn't change) can be used to request a new access-token. If access
// is revoked by identity provider, or no refresh token is set request will return an error
accessToken, expiry, err := p.AuthenticateRefresh(session.RefreshToken)
if err != nil {
log.FromRequest(r).Warn().
Str("RefreshToken", session.RefreshToken).
Str("AccessToken", session.AccessToken).
Msg("proxy.Authenticate: refresh failure")
return err
}
if !ok {
return ErrUserNotAuthorized
}
} else if session.ValidationPeriodExpired() {
ok := p.authenticateClient.ValidateSessionState(session)
if !ok {
return ErrUserNotAuthorized
}
session.AccessToken = accessToken
session.RefreshDeadline = expiry
log.FromRequest(r).Info().
Str("RefreshToken", session.RefreshToken).
Str("AccessToken", session.AccessToken).
Msg("proxy.Authenticate: refresh success")
}
err = p.sessionStore.SaveSession(w, r, session)
if err != nil {
return err
}
// pass user & user-email details to client applications
r.Header.Set(HeaderUserID, session.User)
r.Header.Set(HeaderEmail, session.Email)
// This user has been OK'd. Allow the request!
return nil
}
@ -316,7 +343,6 @@ func (p *Proxy) router(r *http.Request) (http.Handler, bool) {
// GetRedirectURL returns the redirect url for a given Proxy,
// setting the scheme to be https if CookieSecure is true.
func (p *Proxy) GetRedirectURL(host string) *url.URL {
// TODO: Ensure that we only allow valid upstream hosts in redirect URIs
u := p.redirectURL
// Build redirect URI from request host
if u.Scheme == "" {
@ -326,19 +352,39 @@ func (p *Proxy) GetRedirectURL(host string) *url.URL {
return u
}
func (p *Proxy) redeemCode(host, code string) (*sessions.SessionState, error) {
if code == "" {
return nil, errors.New("missing code")
}
redirectURL := p.GetRedirectURL(host)
s, err := p.authenticateClient.Redeem(redirectURL.String(), code)
if err != nil {
return s, err
}
if s.Email == "" {
return s, errors.New("invalid email address")
}
return s, nil
// signRedirectURL signs the redirect url string, given a timestamp, and returns it
func (p *Proxy) signRedirectURL(rawRedirect string, timestamp time.Time) string {
h := hmac.New(sha256.New, []byte(p.SharedKey))
h.Write([]byte(rawRedirect))
h.Write([]byte(fmt.Sprint(timestamp.Unix())))
return base64.URLEncoding.EncodeToString(h.Sum(nil))
}
// GetSignInURL with typical oauth parameters
func (p *Proxy) GetSignInURL(authenticateURL, redirectURL *url.URL, state string) *url.URL {
a := authenticateURL.ResolveReference(&url.URL{Path: "/sign_in"})
now := time.Now()
rawRedirect := redirectURL.String()
params, _ := url.ParseQuery(a.RawQuery)
params.Set("redirect_uri", rawRedirect)
params.Set("shared_secret", p.SharedKey)
params.Set("response_type", "code")
params.Add("state", state)
params.Set("ts", fmt.Sprint(now.Unix()))
params.Set("sig", p.signRedirectURL(rawRedirect, now))
a.RawQuery = params.Encode()
return a
}
// GetSignOutURL creates and returns the sign out URL, given a redirectURL
func (p *Proxy) GetSignOutURL(authenticateURL, redirectURL *url.URL) *url.URL {
a := authenticateURL.ResolveReference(&url.URL{Path: "/sign_out"})
now := time.Now()
rawRedirect := redirectURL.String()
params, _ := url.ParseQuery(a.RawQuery)
params.Add("redirect_uri", rawRedirect)
params.Set("ts", fmt.Sprint(now.Unix()))
params.Set("sig", p.signRedirectURL(rawRedirect, now))
a.RawQuery = params.Encode()
return a
}

View file

@ -1,6 +1,8 @@
package proxy // import "github.com/pomerium/pomerium/proxy"
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
@ -13,11 +15,15 @@ import (
"time"
"github.com/pomerium/envconfig"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/templates"
"github.com/pomerium/pomerium/proxy/authenticator"
pb "github.com/pomerium/pomerium/proto/authenticate"
)
const (
@ -29,10 +35,13 @@ const (
HeaderEmail = "x-pomerium-authenticated-user-email"
)
// Options represents the configuration options for the proxy service.
// Options represents the configurations available for the proxy service.
type Options struct {
// AuthenticateServiceURL specifies the url to the pomerium authenticate http service.
AuthenticateServiceURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
// Authenticate service settings
AuthenticateURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
AuthenticateInternalURL string `envconfig:"AUTHENTICATE_INTERNAL_URL"`
//
OverideCertificateName string `envconfig:"OVERIDE_CERTIFICATE_NAME"`
// SigningKey is a base64 encoded private key used to add a JWT-signature to proxied requests.
// See : https://www.pomerium.io/guide/signed-headers.html
@ -40,37 +49,33 @@ type Options struct {
// SharedKey is a 32 byte random key used to authenticate access between services.
SharedKey string `envconfig:"SHARED_SECRET"`
DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"`
// Session/Cookie management
CookieName string
CookieSecret string `envconfig:"COOKIE_SECRET"`
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
CookieSecure bool `envconfig:"COOKIE_SECURE"`
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
CookieLifetimeTTL time.Duration `envconfig:"COOKIE_LIFETIME"`
CookieName string `envconfig:"COOKIE_NAME"`
CookieSecret string `envconfig:"COOKIE_SECRET"`
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
PassAccessToken bool `envconfig:"PASS_ACCESS_TOKEN"`
// session details
SessionValidTTL time.Duration `envconfig:"SESSION_VALID_TTL"`
SessionLifetimeTTL time.Duration `envconfig:"SESSION_LIFETIME_TTL"`
GracePeriodTTL time.Duration `envconfig:"GRACE_PERIOD_TTL"`
Routes map[string]string `envconfig:"ROUTES"`
// Sub-routes
Routes map[string]string `envconfig:"ROUTES"`
DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"`
}
// NewOptions returns a new options struct
var defaultOptions = &Options{
CookieName: "_pomerium_proxy",
CookieHTTPOnly: false,
CookieHTTPOnly: true,
CookieSecure: true,
CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(30) * time.Minute,
CookieLifetimeTTL: time.Duration(720) * time.Hour,
DefaultUpstreamTimeout: time.Duration(10) * time.Second,
SessionLifetimeTTL: time.Duration(720) * time.Hour,
SessionValidTTL: time.Duration(1) * time.Minute,
GracePeriodTTL: time.Duration(3) * time.Hour,
PassAccessToken: false,
}
// OptionsFromEnvConfig builds the authentication service's configuration
// OptionsFromEnvConfig builds the IdentityProvider service's configuration
// options from provided environmental variables
func OptionsFromEnvConfig() (*Options, error) {
o := defaultOptions
@ -94,11 +99,11 @@ func (o *Options) Validate() error {
return fmt.Errorf("could not parse destination %s as url : %q", to, err)
}
}
if o.AuthenticateServiceURL == nil {
return errors.New("missing setting: provider-url")
if o.AuthenticateURL == nil {
return errors.New("missing setting: authenticate-service-url")
}
if o.AuthenticateServiceURL.Scheme != "https" {
return errors.New("provider-url must be a valid https url")
if o.AuthenticateURL.Scheme != "https" {
return errors.New("authenticate-service-url must be a valid https url")
}
if o.CookieSecret == "" {
return errors.New("missing setting: cookie-secret")
@ -124,26 +129,29 @@ func (o *Options) Validate() error {
// Proxy stores all the information associated with proxying a request.
type Proxy struct {
PassAccessToken bool
SharedKey string
// services
authenticateClient *authenticator.AuthenticateClient
// Authenticate Service Configuration
AuthenticateURL *url.URL
AuthenticateInternalURL string
AuthenticatorClient pb.AuthenticatorClient
// AuthenticateConn must be closed by Proxy's caller
AuthenticateConn *grpc.ClientConn
OverideCertificateName string
// session
cipher cryptutil.Cipher
csrfStore sessions.CSRFStore
sessionStore sessions.SessionStore
cipher cryptutil.Cipher
csrfStore sessions.CSRFStore
sessionStore sessions.SessionStore
CookieExpire time.Duration
CookieRefresh time.Duration
CookieLifetimeTTL time.Duration
redirectURL *url.URL
templates *template.Template
mux map[string]http.Handler
}
// StateParameter holds the redirect id along with the session id.
type StateParameter struct {
SessionID string `json:"session_id"`
RedirectURI string `json:"redirect_uri"`
}
// New takes a Proxy service from options and a validation function.
// Function returns an error if options fail to validate.
func New(opts *Options) (*Proxy, error) {
@ -173,27 +181,21 @@ func New(opts *Options) (*Proxy, error) {
return nil, err
}
authClient := authenticator.NewClient(
opts.AuthenticateServiceURL,
opts.SharedKey,
// todo(bdd): fields below should be passed as function args
opts.SessionValidTTL,
opts.SessionLifetimeTTL,
opts.GracePeriodTTL,
)
p := &Proxy{
// these fields make up the routing mechanism
mux: make(map[string]http.Handler),
// session state
cipher: cipher,
csrfStore: cookieStore,
sessionStore: cookieStore,
authenticateClient: authClient,
redirectURL: &url.URL{Path: "/.pomerium/callback"},
templates: templates.New(),
PassAccessToken: opts.PassAccessToken,
cipher: cipher,
csrfStore: cookieStore,
sessionStore: cookieStore,
AuthenticateURL: opts.AuthenticateURL,
AuthenticateInternalURL: opts.AuthenticateInternalURL,
OverideCertificateName: opts.OverideCertificateName,
SharedKey: opts.SharedKey,
redirectURL: &url.URL{Path: "/.pomerium/callback"},
templates: templates.New(),
CookieExpire: opts.CookieExpire,
CookieLifetimeTTL: opts.CookieLifetimeTTL,
}
for from, to := range opts.Routes {
@ -205,8 +207,42 @@ func New(opts *Options) (*Proxy, error) {
return nil, err
}
p.Handle(fromURL.Host, handler)
log.Info().Str("from", fromURL.Host).Str("to", toURL.String()).Msg("proxy.New : route")
log.Info().Str("from", fromURL.Host).Str("to", toURL.String()).Msg("proxy.New: new route")
}
// if no port given, assume https/443
port := p.AuthenticateURL.Port()
if port == "" {
port = "443"
}
authEndpoint := fmt.Sprintf("%s:%s", p.AuthenticateURL.Host, port)
cp, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
if p.AuthenticateInternalURL != "" {
authEndpoint = p.AuthenticateInternalURL
}
log.Info().Str("authEndpoint", authEndpoint).Msgf("proxy.New: grpc authenticate connection")
cert := credentials.NewTLS(&tls.Config{RootCAs: cp})
if p.OverideCertificateName != "" {
err = cert.OverrideServerName(p.OverideCertificateName)
if err != nil {
return nil, err
}
}
grpcAuth := middleware.NewSharedSecretCred(p.SharedKey)
p.AuthenticateConn, err = grpc.Dial(
authEndpoint,
grpc.WithTransportCredentials(cert),
grpc.WithPerRPCCredentials(grpcAuth),
)
if err != nil {
return nil, err
}
p.AuthenticatorClient = pb.NewAuthenticatorClient(p.AuthenticateConn)
return p, nil
}
@ -260,8 +296,8 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
u.handler.ServeHTTP(w, r)
}
// NewReverseProxy creates a reverse proxy to a specified url.
// It adds an X-Forwarded-Host header that is the request's host.
// NewReverseProxy returns a new ReverseProxy that routes URLs to the scheme, host, and
// base path provided in target. NewReverseProxy rewrites the Host header.
func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
proxy := httputil.NewSingleHostReverseProxy(to)
proxy.Transport = defaultUpstreamTransport

View file

@ -11,10 +11,9 @@ import (
"testing"
)
func init() {
os.Clearenv()
}
func TestOptionsFromEnvConfig(t *testing.T) {
os.Clearenv()
tests := []struct {
name string
want *Options
@ -23,9 +22,9 @@ func TestOptionsFromEnvConfig(t *testing.T) {
wantErr bool
}{
{"good default, no env settings", defaultOptions, "", "", false},
{"bad url", nil, "AUTHENTICATE_SERVICE_URL", "%.rjlw", true},
{"good duration", defaultOptions, "SESSION_VALID_TTL", "1m", false},
{"bad duration", nil, "SESSION_VALID_TTL", "1sm", true},
{"bad url", nil, "AUTHENTICATE_SERVICE_URL", "%.ugly", true},
{"good duration", defaultOptions, "COOKIE_REFRESH", "1m", false},
{"bad duration", nil, "COOKIE_REFRESH", "1sm", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -46,6 +45,8 @@ func TestOptionsFromEnvConfig(t *testing.T) {
}
func Test_urlParse(t *testing.T) {
os.Clearenv()
tests := []struct {
name string
uri string
@ -131,10 +132,10 @@ func TestNewReverseProxyHandler(t *testing.T) {
func testOptions() *Options {
authurl, _ := url.Parse("https://sso-auth.corp.beyondperimeter.com")
return &Options{
Routes: map[string]string{"corp.example.com": "example.com"},
AuthenticateServiceURL: authurl,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
Routes: map[string]string{"corp.example.com": "example.com"},
AuthenticateURL: authurl,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
}
}
@ -145,10 +146,10 @@ func TestOptions_Validate(t *testing.T) {
badToRoute := testOptions()
badToRoute.Routes = map[string]string{"^": "example.com"}
badAuthURL := testOptions()
badAuthURL.AuthenticateServiceURL = nil
badAuthURL.AuthenticateURL = nil
authurl, _ := url.Parse("http://sso-auth.corp.beyondperimeter.com")
httpAuthURL := testOptions()
httpAuthURL.AuthenticateServiceURL = authurl
httpAuthURL.AuthenticateURL = authurl
emptyCookieSecret := testOptions()
emptyCookieSecret.CookieSecret = ""
invalidCookieSecret := testOptions()

View file

@ -3,12 +3,33 @@
# resources to avoid being billed. For reference, this tutorial cost me <10 cents for a couple of hours.
# create a cluster
gcloud container clusters create pomerium
gcloud container clusters create pomerium --num-nodes 1
# get cluster credentials os we can use kubctl locally
gcloud container clusters get-credentials pomerium
# create `pomerium` namespace
kubectl create ns pomerium
######################################################################
#### UNCOMMENT to use helm to install cert-manager & nginx-ingress####
######################################################################
# setup service account for tiller used by helm
# kubectl create serviceaccount --namespace kube-system tiller
# kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
# helm init --service-account tiller
# # update helm
# helm repo update
# kubectl get deployments -n kube-system
# # create nginx-ingress
# helm install --name nginx-ingress stable/nginx-ingress --set rbac.create=true
# # install cert-manager to auto grab lets encrypt certificates
# kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.6/deploy/manifests/00-crds.yaml
# helm repo update
# helm install --name cert-manager --namespace cert-manager stable/cert-manager
# configure Lets Encrypt Issuer
# kubectl apply -f docs/docs/examples/kubernetes/issuer.le.prod.yml
# kubectl apply -f docs/docs/examples/kubernetes/issuer.le.stage.yml
# kubectl get certificate
# create our cryptographically random keys
kubectl create secret generic -n pomerium shared-secret --from-literal=shared-secret=$(head -c32 /dev/urandom | base64)
kubectl create secret generic -n pomerium cookie-secret --from-literal=cookie-secret=$(head -c32 /dev/urandom | base64)
@ -23,16 +44,19 @@ kubectl create secret tls -n pomerium pomerium-tls --key privkey.pem --cert cert
# !!! IMPORTANT !!!
# YOU MUST CHANGE THE Identity Provider Client Secret
# !!! IMPORTANT !!!
# kubectl create secret generic -n pomerium idp-client-secret --from-literal=idp-client-secret=REPLACE_ME
# kubectl create secret generic -n pomerium idp-client-secret --from-literal=REPLACE_ME
# Create the proxy & authenticate deployment
kubectl create -f docs/docs/examples/kubernetes/authenticate.deploy.yml
kubectl create -f docs/docs/examples/kubernetes/proxy.deploy.yml
kubectl apply -f docs/docs/examples/kubernetes/authenticate.deploy.yml
kubectl apply -f docs/docs/examples/kubernetes/proxy.deploy.yml
# Create the proxy & authenticate services
kubectl apply -f docs/docs/examples/kubernetes/proxy.service.yml
kubectl apply -f docs/docs/examples/kubernetes/authenticate.service.yml
# Create and apply the Ingress; this is GKE specific
kubectl apply -f docs/docs/examples/kubernetes/ingress.yml
# Alternatively, nginx-ingress can be used
# kubectl apply -f docs/docs/examples/kubernetes/ingress.nginx.yml
# When done, clean up by deleting the cluster!
# gcloud container clusters delete pomerium