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 The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR

View file

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

View file

@ -8,23 +8,19 @@ import (
"time" "time"
) )
func init() {
os.Clearenv()
}
func testOptions() *Options { func testOptions() *Options {
redirectURL, _ := url.Parse("https://example.com/oauth2/callback") redirectURL, _ := url.Parse("https://example.com/oauth2/callback")
return &Options{ return &Options{
ProxyRootDomains: []string{"example.com"}, ProxyRootDomains: []string{"example.com"},
AllowedDomains: []string{"example.com"}, AllowedDomains: []string{"example.com"},
RedirectURL: redirectURL, RedirectURL: redirectURL,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=", SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
ClientID: "test-client-id", ClientID: "test-client-id",
ClientSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=", ClientSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=", CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieRefresh: time.Duration(1) * time.Hour, CookieRefresh: time.Duration(1) * time.Hour,
SessionLifetimeTTL: time.Duration(720) * time.Hour, CookieLifetimeTTL: time.Duration(720) * time.Hour,
CookieExpire: time.Duration(168) * time.Hour, CookieExpire: time.Duration(168) * time.Hour,
} }
} }
@ -81,6 +77,8 @@ func TestOptions_Validate(t *testing.T) {
} }
func TestOptionsFromEnvConfig(t *testing.T) { func TestOptionsFromEnvConfig(t *testing.T) {
os.Clearenv()
tests := []struct { tests := []struct {
name string name string
want *Options want *Options
@ -91,7 +89,7 @@ func TestOptionsFromEnvConfig(t *testing.T) {
{"good default, no env settings", defaultOptions, "", "", false}, {"good default, no env settings", defaultOptions, "", "", false},
{"bad url", nil, "REDIRECT_URL", "%.rjlw", true}, {"bad url", nil, "REDIRECT_URL", "%.rjlw", true},
{"good duration", defaultOptions, "COOKIE_EXPIRE", "1m", false}, {"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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 ( import (
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@ -17,16 +16,19 @@ import (
"github.com/pomerium/pomerium/internal/version" "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{ var securityHeaders = map[string]string{
"Strict-Transport-Security": "max-age=31536000", "Strict-Transport-Security": "max-age=31536000",
"X-Frame-Options": "DENY", "X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff", "X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block", "X-XSS-Protection": "1; mode=block",
"Content-Security-Policy": "default-src 'none'; style-src 'self' 'sha256-pSTVzZsFAqd2U3QYu+BoBDtuJWaPM/+qMy/dBRrhb5Y='; img-src 'self';", "Content-Security-Policy": "default-src 'none'; style-src 'self' " +
"Referrer-Policy": "Same-origin", "'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 { func (p *Authenticate) Handler() http.Handler {
// set up our standard middlewares // set up our standard middlewares
stdMiddleware := middleware.NewChain() stdMiddleware := middleware.NewChain()
@ -52,8 +54,6 @@ func (p *Authenticate) Handler() http.Handler {
middleware.ValidateSignature(p.SharedKey), middleware.ValidateSignature(p.SharedKey),
middleware.ValidateRedirectURI(p.ProxyRootDomains)) middleware.ValidateRedirectURI(p.ProxyRootDomains))
validateClientSecretMiddleware := stdMiddleware.Append(middleware.ValidateClientSecret(p.SharedKey))
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/robots.txt", stdMiddleware.ThenFunc(p.RobotsTxt)) mux.Handle("/robots.txt", stdMiddleware.ThenFunc(p.RobotsTxt))
// Identity Provider (IdP) callback endpoints and callbacks // 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)) mux.Handle("/oauth2/callback", stdMiddleware.ThenFunc(p.OAuthCallback))
// authenticate-server endpoints // authenticate-server endpoints
mux.Handle("/sign_in", validateSignatureMiddleware.ThenFunc(p.SignIn)) mux.Handle("/sign_in", validateSignatureMiddleware.ThenFunc(p.SignIn))
mux.Handle("/sign_out", validateSignatureMiddleware.ThenFunc(p.SignOut)) // "GET", "POST" 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
return mux return mux
} }
@ -76,43 +72,15 @@ func (p *Authenticate) RobotsTxt(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "User-agent: *\nDisallow: /") 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) { func (p *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*sessions.SessionState, error) {
session, err := p.sessionStore.LoadSession(r) session, err := p.sessionStore.LoadSession(r)
if err != nil { 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) p.sessionStore.ClearSession(w, r)
return nil, err return nil, err
} }
// ensure sessions lifetime has not expired // if long-lived lifetime has expired, clear session
if session.LifetimePeriodExpired() { if session.LifetimePeriodExpired() {
log.FromRequest(r).Warn().Msg("authenticate: lifetime expired") log.FromRequest(r).Warn().Msg("authenticate: lifetime expired")
p.sessionStore.ClearSession(w, r) 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 // check if session refresh period is up
if session.RefreshPeriodExpired() { if session.RefreshPeriodExpired() {
ok, err := p.provider.RefreshSessionIfNeeded(session) newToken, err := p.provider.Refresh(session.RefreshToken)
if err != nil { if err != nil {
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed to refresh session") log.FromRequest(r).Error().Err(err).Msg("authenticate: failed to refresh session")
p.sessionStore.ClearSession(w, r) p.sessionStore.ClearSession(w, r)
return nil, err return nil, err
} }
if !ok { session.AccessToken = newToken.AccessToken
log.FromRequest(r).Error().Msg("user unauthorized after refresh") session.RefreshDeadline = newToken.Expiry
p.sessionStore.ClearSession(w, r)
return nil, httputil.ErrUserNotAuthorized
}
// update refresh'd session in cookie
err = p.sessionStore.SaveSession(w, r, session) err = p.sessionStore.SaveSession(w, r, session)
if err != nil { if err != nil {
// We refreshed the session successfully, but failed to save it. // 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 { } else {
// The session has not exceeded it's lifetime or requires refresh // The session has not exceeded it's lifetime or requires refresh
ok := p.provider.ValidateSessionState(session) ok, err := p.provider.Validate(session.IDToken)
if !ok { if !ok || err != nil {
log.FromRequest(r).Error().Msg("invalid session state") log.FromRequest(r).Error().Err(err).Msg("invalid session state")
p.sessionStore.ClearSession(w, r) p.sessionStore.ClearSession(w, r)
return nil, httputil.ErrUserNotAuthorized 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) { if !p.Validator(session.Email) {
log.FromRequest(r).Error().Msg("invalid email user") log.FromRequest(r).Error().Msg("invalid email user")
return nil, httputil.ErrUserNotAuthorized return nil, httputil.ErrUserNotAuthorized
} }
log.Info().Msg("authenticate")
return session, nil return session, nil
} }
// SignIn handles the /sign_in endpoint. It attempts to authenticate the user, // 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. // and if the user is not authenticated, it renders a sign in page.
func (p *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) { 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) session, err := p.authenticate(w, r)
switch err { switch err {
case nil: case nil:
// User is authenticated, redirect back to proxy // User is authenticated, redirect back to proxy
p.ProxyOAuthRedirect(w, r, session) p.ProxyOAuthRedirect(w, r, session)
case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession: 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 { if err != http.ErrNoCookie {
p.sessionStore.ClearSession(w, r) p.sessionStore.ClearSession(w, r)
} }
p.OAuthStart(w, r) p.OAuthStart(w, r)
default: 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)) 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) { 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() err := r.ParseForm()
if err != nil { if err != nil {
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return return
} }
// original `state` parameter received from the proxy application.
state := r.Form.Get("state") state := r.Form.Get("state")
if state == "" { if state == "" {
httputil.ErrorResponse(w, r, "no state parameter supplied", http.StatusForbidden) httputil.ErrorResponse(w, r, "no state parameter supplied", http.StatusForbidden)
return return
} }
// redirect url of proxy-service
redirectURI := r.Form.Get("redirect_uri") redirectURI := r.Form.Get("redirect_uri")
if redirectURI == "" { if redirectURI == "" {
httputil.ErrorResponse(w, r, "no redirect_uri parameter supplied", http.StatusForbidden) 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) httputil.ErrorResponse(w, r, "malformed redirect_uri parameter passed", http.StatusBadRequest)
return return
} }
// encrypt session state as json blob
encrypted, err := sessions.MarshalSession(session, p.cipher) encrypted, err := sessions.MarshalSession(session, p.cipher)
if err != nil { if err != nil {
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError) httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
@ -267,6 +216,7 @@ func (p *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
case nil: case nil:
break break
case http.ErrNoCookie: // if there's no cookie in the session we can just redirect 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) http.Redirect(w, r, redirectURI, http.StatusFound)
return return
default: default:
@ -277,7 +227,7 @@ func (p *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
return return
} }
err = p.provider.Revoke(session) err = p.provider.Revoke(session.AccessToken)
if err != nil { if err != nil {
log.Error().Err(err).Msg("authenticate.SignOut : error revoking session") log.Error().Err(err).Msg("authenticate.SignOut : error revoking session")
p.SignOutPage(w, r, "An error occurred during sign out. Please try again.") 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") signature := r.Form.Get("sig")
timestamp := r.Form.Get("ts") 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 // 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) 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) p.templates.ExecuteTemplate(w, "sign_out.html", t)
} }
// OAuthStart starts the authentication process by redirecting to the provider. It provides a // 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 authentication. // `redirectURI`, allowing the provider to redirect back to the sso proxy after authenticate.
func (p *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) { func (p *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
authRedirectURL, err := url.Parse(r.URL.Query().Get("redirect_uri")) authRedirectURL, err := url.Parse(r.URL.Query().Get("redirect_uri"))
if err != nil { if err != nil {
@ -339,12 +290,12 @@ func (p *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
nonce := fmt.Sprintf("%x", cryptutil.GenerateKey()) nonce := fmt.Sprintf("%x", cryptutil.GenerateKey())
p.csrfStore.SetCSRF(w, r, nonce) 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) { if !middleware.ValidRedirectURI(authRedirectURL.String(), p.ProxyRootDomains) {
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest) httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
return 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")) proxyRedirectURL, err := url.Parse(authRedirectURL.Query().Get("redirect_uri"))
if err != nil || !middleware.ValidRedirectURI(proxyRedirectURL.String(), p.ProxyRootDomains) { if err != nil || !middleware.ValidRedirectURI(proxyRedirectURL.String(), p.ProxyRootDomains) {
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest) httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
@ -359,51 +310,41 @@ func (p *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
return 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()))) state := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", nonce, authRedirectURL.String())))
// build the provider sign in url
signInURL := p.provider.GetSignInURL(state) signInURL := p.provider.GetSignInURL(state)
http.Redirect(w, r, signInURL, http.StatusFound) 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 // getOAuthCallback completes the oauth cycle from an identity provider's callback
func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) (string, error) { func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) (string, error) {
// finish the oauth cycle
err := r.ParseForm() err := r.ParseForm()
if err != nil { 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()} return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: err.Error()}
} }
errorString := r.Form.Get("error") errorString := r.Form.Get("error")
if errorString != "" { if errorString != "" {
log.FromRequest(r).Error().Err(err).Msg("authenticate: provider returned error")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: errorString} return "", httputil.HTTPError{Code: http.StatusForbidden, Message: errorString}
} }
code := r.Form.Get("code") code := r.Form.Get("code")
if code == "" { if code == "" {
log.FromRequest(r).Error().Err(err).Msg("authenticate: provider missing code")
return "", httputil.HTTPError{Code: http.StatusBadRequest, Message: "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 { 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()} return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: err.Error()}
} }
bytes, err := base64.URLEncoding.DecodeString(r.Form.Get("state")) bytes, err := base64.URLEncoding.DecodeString(r.Form.Get("state"))
if err != nil { 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"} return "", httputil.HTTPError{Code: http.StatusBadRequest, Message: "Couldn't decode state"}
} }
s := strings.SplitN(string(bytes), ":", 2) s := strings.SplitN(string(bytes), ":", 2)
@ -414,11 +355,12 @@ func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
redirect := s[1] redirect := s[1]
c, err := p.csrfStore.GetCSRF(r) c, err := p.csrfStore.GetCSRF(r)
if err != nil { if err != nil {
log.FromRequest(r).Error().Err(err).Msg("authenticate: bad csrf")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Missing CSRF token"} return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Missing CSRF token"}
} }
p.csrfStore.ClearCSRF(w, r) p.csrfStore.ClearCSRF(w, r)
if c.Value != nonce { 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"} 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 // 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) { if !p.Validator(session.Email) {
log.FromRequest(r).Error().Err(err).Str("email", session.Email).Msg("invalid email permissions denied") 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"} 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) err = p.sessionStore.SaveSession(w, r, session)
if err != nil { if err != nil {
log.Error().Err(err).Msg("internal error") log.Error().Err(err).Msg("internal error")
@ -442,182 +381,22 @@ func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
return redirect, nil return redirect, nil
} }
// OAuthCallback handles the callback from the provider, and returns an error response if there is an error. // OAuthCallback handles the callback from the identity provider. Displays an error page if there
// If there is no error it will redirect to the redirect url. // 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) { func (p *Authenticate) OAuthCallback(w http.ResponseWriter, r *http.Request) {
redirect, err := p.getOAuthCallback(w, r) redirect, err := p.getOAuthCallback(w, r)
switch h := err.(type) { switch h := err.(type) {
case nil: case nil:
break break
case httputil.HTTPError: case httputil.HTTPError:
log.Error().Err(err).Msg("authenticate: oauth callback error")
httputil.ErrorResponse(w, r, h.Message, h.Code) httputil.ErrorResponse(w, r, h.Message, h.Code)
return return
default: 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) httputil.ErrorResponse(w, r, "Internal Error", http.StatusInternalServerError)
return return
} }
// redirect back to the proxy-service
http.Redirect(w, r, redirect, http.StatusFound) 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 package authenticate
import ( import (
"bytes"
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"testing" "testing"
"github.com/pomerium/pomerium/authenticate/providers"
"github.com/pomerium/pomerium/internal/templates" "github.com/pomerium/pomerium/internal/templates"
) )
@ -19,7 +17,6 @@ func testAuthenticate() *Authenticate {
auth.AllowedDomains = []string{"*"} auth.AllowedDomains = []string{"*"}
auth.ProxyRootDomains = []string{"example.com"} auth.ProxyRootDomains = []string{"example.com"}
auth.templates = templates.New() auth.templates = templates.New()
auth.provider = providers.NewTestProvider(auth.RedirectURL)
return &auth return &auth
} }
@ -38,43 +35,5 @@ func TestAuthenticate_RobotsTxt(t *testing.T) {
expected := fmt.Sprintf("User-agent: *\nDisallow: /") expected := fmt.Sprintf("User-agent: *\nDisallow: /")
if rr.Body.String() != expected { if rr.Body.String() != expected {
t.Errorf("handler returned wrong body: got %v want %v", 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. // GitlabProvider is an implementation of the Provider interface.
type GitlabProvider struct { type GitlabProvider struct {
*ProviderData *IdentityProvider
cb *circuit.Breaker cb *circuit.Breaker
} }
@ -32,7 +32,7 @@ type GitlabProvider struct {
// - https://docs.gitlab.com/ee/integration/oauth_provider.html // - https://docs.gitlab.com/ee/integration/oauth_provider.html
// - https://docs.gitlab.com/ee/api/oauth2.html // - https://docs.gitlab.com/ee/api/oauth2.html
// - https://gitlab.com/.well-known/openid-configuration // - https://gitlab.com/.well-known/openid-configuration
func NewGitlabProvider(p *ProviderData) (*GitlabProvider, error) { func NewGitlabProvider(p *IdentityProvider) (*GitlabProvider, error) {
ctx := context.Background() ctx := context.Background()
if p.ProviderURL == "" { if p.ProviderURL == "" {
p.ProviderURL = defaultGitlabProviderURL p.ProviderURL = defaultGitlabProviderURL
@ -42,8 +42,9 @@ func NewGitlabProvider(p *ProviderData) (*GitlabProvider, error) {
if err != nil { if err != nil {
return nil, err 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.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{ p.oauth = &oauth2.Config{
ClientID: p.ClientID, ClientID: p.ClientID,
@ -53,7 +54,7 @@ func NewGitlabProvider(p *ProviderData) (*GitlabProvider, error) {
Scopes: p.Scopes, Scopes: p.Scopes,
} }
gitlabProvider := &GitlabProvider{ gitlabProvider := &GitlabProvider{
ProviderData: p, IdentityProvider: p,
} }
gitlabProvider.cb = circuit.NewBreaker(&circuit.Options{ gitlabProvider.cb = circuit.NewBreaker(&circuit.Options{
HalfOpenConcurrentRequests: 2, HalfOpenConcurrentRequests: 2,

View file

@ -11,7 +11,6 @@ import (
"github.com/pomerium/pomerium/authenticate/circuit" "github.com/pomerium/pomerium/authenticate/circuit"
"github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version" "github.com/pomerium/pomerium/internal/version"
) )
@ -19,14 +18,14 @@ const defaultGoogleProviderURL = "https://accounts.google.com"
// GoogleProvider is an implementation of the Provider interface. // GoogleProvider is an implementation of the Provider interface.
type GoogleProvider struct { type GoogleProvider struct {
*ProviderData *IdentityProvider
cb *circuit.Breaker cb *circuit.Breaker
// non-standard oidc fields // non-standard oidc fields
RevokeURL *url.URL RevokeURL *url.URL
} }
// NewGoogleProvider returns a new GoogleProvider and sets the provider url endpoints. // 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() ctx := context.Background()
if p.ProviderURL == "" { if p.ProviderURL == "" {
@ -37,18 +36,20 @@ func NewGoogleProvider(p *ProviderData) (*GoogleProvider, error) {
if err != nil { if err != nil {
return nil, err 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.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{ p.oauth = &oauth2.Config{
ClientID: p.ClientID, ClientID: p.ClientID,
ClientSecret: p.ClientSecret, ClientSecret: p.ClientSecret,
Endpoint: p.provider.Endpoint(), Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(), RedirectURL: p.RedirectURL.String(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, Scopes: p.Scopes,
} }
googleProvider := &GoogleProvider{ googleProvider := &GoogleProvider{
ProviderData: p, IdentityProvider: p,
} }
// google supports a revocation endpoint // google supports a revocation endpoint
var claims struct { var claims struct {
@ -91,9 +92,9 @@ func (p *GoogleProvider) cbStateChange(from, to circuit.State) {
// //
// https://developers.google.com/identity/protocols/OAuth2WebServer#tokenrevoke // https://developers.google.com/identity/protocols/OAuth2WebServer#tokenrevoke
// https://github.com/googleapis/google-api-dotnet-client/issues/1285 // 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 := url.Values{}
params.Add("token", s.AccessToken) params.Add("token", accessToken)
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil) err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil)
if err != nil && err != httputil.ErrTokenRevoked { if err != nil && err != httputil.ErrTokenRevoked {
return err return err
@ -105,4 +106,5 @@ func (p *GoogleProvider) Revoke(s *sessions.SessionState) error {
// Google requires access type offline // Google requires access type offline
func (p *GoogleProvider) GetSignInURL(state string) string { func (p *GoogleProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce) 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/authenticate/circuit"
"github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version" "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 // AzureProvider is an implementation of the Provider interface
type AzureProvider struct { type AzureProvider struct {
*ProviderData *IdentityProvider
cb *circuit.Breaker cb *circuit.Breaker
// non-standard oidc fields // non-standard oidc fields
RevokeURL *url.URL RevokeURL *url.URL
@ -31,7 +30,7 @@ type AzureProvider struct {
// NewAzureProvider returns a new AzureProvider and sets the provider url endpoints. // NewAzureProvider returns a new AzureProvider and sets the provider url endpoints.
// If non-"common" tenant is desired, ProviderURL must be set. // If non-"common" tenant is desired, ProviderURL must be set.
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc // 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() ctx := context.Background()
if p.ProviderURL == "" { if p.ProviderURL == "" {
@ -43,18 +42,20 @@ func NewAzureProvider(p *ProviderData) (*AzureProvider, error) {
if err != nil { if err != nil {
return nil, err 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.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{ p.oauth = &oauth2.Config{
ClientID: p.ClientID, ClientID: p.ClientID,
ClientSecret: p.ClientSecret, ClientSecret: p.ClientSecret,
Endpoint: p.provider.Endpoint(), Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(), RedirectURL: p.RedirectURL.String(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, Scopes: p.Scopes,
} }
azureProvider := &AzureProvider{ azureProvider := &AzureProvider{
ProviderData: p, IdentityProvider: p,
} }
// azure has a "end session endpoint" // azure has a "end session endpoint"
var claims struct { var claims struct {
@ -95,9 +96,9 @@ func (p *AzureProvider) cbStateChange(from, to circuit.State) {
// Revoke revokes the access token a given session 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 //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 := url.Values{}
params.Add("token", s.AccessToken) params.Add("token", token)
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil) err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil)
if err != nil && err != httputil.ErrTokenRevoked { if err != nil && err != httputil.ErrTokenRevoked {
return err return err

View file

@ -12,11 +12,11 @@ import (
// of an authorization identity provider. // of an authorization identity provider.
// see : https://openid.net/specs/openid-connect-core-1_0.html // see : https://openid.net/specs/openid-connect-core-1_0.html
type OIDCProvider struct { type OIDCProvider struct {
*ProviderData *IdentityProvider
} }
// NewOIDCProvider creates a new instance of an OpenID Connect provider. // 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() ctx := context.Background()
if p.ProviderURL == "" { if p.ProviderURL == "" {
return nil, errors.New("missing required provider url") return nil, errors.New("missing required provider url")
@ -26,13 +26,16 @@ func NewOIDCProvider(p *ProviderData) (*OIDCProvider, error) {
if err != nil { if err != nil {
return nil, err 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.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{ p.oauth = &oauth2.Config{
ClientID: p.ClientID, ClientID: p.ClientID,
ClientSecret: p.ClientSecret, ClientSecret: p.ClientSecret,
Endpoint: p.provider.Endpoint(), Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(), 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" "golang.org/x/oauth2"
"github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version" "github.com/pomerium/pomerium/internal/version"
) )
// OktaProvider provides a standard, OpenID Connect implementation // OktaProvider provides a standard, OpenID Connect implementation
// of an authorization identity provider. // of an authorization identity provider.
type OktaProvider struct { type OktaProvider struct {
*ProviderData *IdentityProvider
// non-standard oidc fields // non-standard oidc fields
RevokeURL *url.URL RevokeURL *url.URL
} }
// NewOktaProvider creates a new instance of an OpenID Connect provider. // 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() ctx := context.Background()
if p.ProviderURL == "" { if p.ProviderURL == "" {
return nil, errors.New("missing required provider url") return nil, errors.New("missing required provider url")
@ -33,24 +32,26 @@ func NewOktaProvider(p *ProviderData) (*OktaProvider, error) {
if err != nil { if err != nil {
return nil, err 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.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{ p.oauth = &oauth2.Config{
ClientID: p.ClientID, ClientID: p.ClientID,
ClientSecret: p.ClientSecret, ClientSecret: p.ClientSecret,
Endpoint: p.provider.Endpoint(), Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(), RedirectURL: p.RedirectURL.String(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, Scopes: p.Scopes,
} }
oktaProvider := OktaProvider{ProviderData: p}
// okta supports a revocation endpoint // okta supports a revocation endpoint
var claims struct { var claims struct {
RevokeURL string `json:"revocation_endpoint"` RevokeURL string `json:"revocation_endpoint"`
} }
if err := p.provider.Claims(&claims); err != nil { if err := p.provider.Claims(&claims); err != nil {
return nil, err return nil, err
} }
oktaProvider := OktaProvider{IdentityProvider: p}
oktaProvider.RevokeURL, err = url.Parse(claims.RevokeURL) oktaProvider.RevokeURL, err = url.Parse(claims.RevokeURL)
if err != nil { if err != nil {
@ -61,11 +62,11 @@ func NewOktaProvider(p *ProviderData) (*OktaProvider, error) {
// Revoke revokes the access token a given session state. // Revoke revokes the access token a given session state.
// https://developer.okta.com/docs/api/resources/oidc#revoke // 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 := url.Values{}
params.Add("client_id", p.ClientID) params.Add("client_id", p.ClientID)
params.Add("client_secret", p.ClientSecret) params.Add("client_secret", p.ClientSecret)
params.Add("token", s.IDToken) params.Add("token", token)
params.Add("token_type_hint", "refresh_token") params.Add("token_type_hint", "refresh_token")
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil) err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil)
if err != nil && err != httputil.ErrTokenRevoked { if err != nil && err != httputil.ErrTokenRevoked {
@ -73,3 +74,9 @@ func (p *OktaProvider) Revoke(s *sessions.SessionState) error {
} }
return nil 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" package providers // import "github.com/pomerium/pomerium/internal/providers"
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"net/http"
"net/url" "net/url"
"time" "time"
@ -32,21 +31,17 @@ const (
// Provider is an interface exposing functions necessary to interact with a given provider. // Provider is an interface exposing functions necessary to interact with a given provider.
type Provider interface { type Provider interface {
Data() *ProviderData Authenticate(string) (*sessions.SessionState, error)
Redeem(string) (*sessions.SessionState, error) Validate(string) (bool, error)
ValidateSessionState(*sessions.SessionState) bool Refresh(string) (*oauth2.Token, error)
Revoke(string) error
GetSignInURL(state string) string 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. // 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. // Returns an error if selected provided not found or if the identity provider is not known.
func New(provider string, pd *ProviderData) (Provider, error) { func New(providerName string, pd *IdentityProvider) (p Provider, err error) {
var err error switch providerName {
var p Provider
switch provider {
case AzureProviderName: case AzureProviderName:
p, err = NewAzureProvider(pd) p, err = NewAzureProvider(pd)
case GitlabProviderName: case GitlabProviderName:
@ -58,7 +53,7 @@ func New(provider string, pd *ProviderData) (Provider, error) {
case OktaProviderName: case OktaProviderName:
p, err = NewOktaProvider(pd) p, err = NewOktaProvider(pd)
default: default:
return nil, fmt.Errorf("authenticate: provider %q not found", provider) return nil, fmt.Errorf("authenticate: %q name not found", providerName)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -66,11 +61,13 @@ func New(provider string, pd *ProviderData) (Provider, error) {
return p, nil return p, nil
} }
// ProviderData holds the fields associated with providers // IdentityProvider contains the fields required for an OAuth 2.0 Authorization Request that
// necessary to implement the Provider interface. // requests that the End-User be authenticated by the Authorization Server.
type ProviderData struct { // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
type IdentityProvider struct {
ProviderName string
RedirectURL *url.URL RedirectURL *url.URL
ProviderName string
ClientID string ClientID string
ClientSecret string ClientSecret string
ProviderURL string ProviderURL string
@ -82,118 +79,50 @@ type ProviderData struct {
oauth *oauth2.Config oauth *oauth2.Config
} }
// Data returns a ProviderData. // GetSignInURL returns a URL to OAuth 2.0 provider's consent page
func (p *ProviderData) Data() *ProviderData { return p } // that asks for permissions for the required scopes explicitly.
//
// GetSignInURL returns the sign in url with typical oauth parameters // State is a token to protect the user from CSRF attacks. You must
func (p *ProviderData) GetSignInURL(state string) string { // 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) 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 // The function verifies it's been signed by the provider, preforms
// any additional checks depending on the Config, and returns the payload. // any additional checks depending on the Config, and returns the payload.
// //
// ValidateSessionState does NOT do nonce validation. // Validate does NOT do nonce validation.
func (p *ProviderData) ValidateSessionState(s *sessions.SessionState) bool { // 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() ctx := context.Background()
_, err := p.verifier.Verify(ctx, s.IDToken) _, err := p.verifier.Verify(ctx, idToken)
if err != nil { if err != nil {
log.Error().Err(err).Msg("authenticate/providers: failed to verify session state") 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 return true, nil
} }
func (p *ProviderData) redeemRefreshToken(s *sessions.SessionState) error { // Authenticate creates a session with an identity provider from a authorization code
log.Info().Msg("authenticate/providers.oidc.redeemRefreshToken 1") func (p *IdentityProvider) Authenticate(code string) (*sessions.SessionState, error) {
ctx := context.Background() ctx := context.Background()
t := &oauth2.Token{ // convert authorization code into a token
RefreshToken: s.RefreshToken, oauth2Token, err := p.oauth.Exchange(ctx, code)
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()
if err != nil { 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(). log.Info().
Str("AccessToken", s.AccessToken). Str("RefreshToken", oauth2Token.RefreshToken).
Str("IdToken", s.IDToken). Str("TokenType", oauth2Token.TokenType).
Time("RefreshDeadline", s.RefreshDeadline). Str("AccessToken", oauth2Token.AccessToken).
Str("RefreshToken", s.RefreshToken). Msg("Authenticate - oauth.Exchange")
Str("Email", s.Email).
Msg("authenticate/providers.redeemRefreshToken")
return nil //id_token contains claims about the authenticated user
} rawIDToken, ok := oauth2Token.Extra("id_token").(string)
func (p *ProviderData) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
rawIDToken, ok := token.Extra("id_token").(string)
if !ok { if !ok {
return nil, fmt.Errorf("token response did not contain an id_token") 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) 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 { var claims struct {
Email string `json:"email"` Email string `json:"email"`
Verified *bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
Groups []string `json:"groups"`
} }
// parse claims from the raw, encoded jwt token // parse claims from the raw, encoded jwt token
if err := idToken.Claims(&claims); err != nil { if err := idToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("authenticate/providers: failed to parse id_token claims: %v", err) 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{ return &sessions.SessionState{
AccessToken: token.AccessToken,
IDToken: rawIDToken, IDToken: rawIDToken,
RefreshToken: token.RefreshToken, AccessToken: oauth2Token.AccessToken,
RefreshDeadline: idToken.Expiry, RefreshToken: oauth2Token.RefreshToken,
LifetimeDeadline: idToken.Expiry, RefreshDeadline: oauth2Token.Expiry,
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
Email: claims.Email, Email: claims.Email,
User: idToken.Subject, User: idToken.Subject,
Groups: claims.Groups,
}, nil }, nil
} }
// RefreshAccessToken allows the service to refresh an access token without // Refresh renews a user's session using an access token without reprompting the user.
// prompting the user for permission. func (p *IdentityProvider) Refresh(refreshToken string) (*oauth2.Token, error) {
func (p *ProviderData) RefreshAccessToken(refreshToken string) (string, time.Duration, error) {
if refreshToken == "" { if refreshToken == "" {
return "", 0, errors.New("authenticate/providers: missing refresh token") return nil, 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},
} }
t := oauth2.Token{RefreshToken: refreshToken} 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(). log.Info().
Str("RefreshToken", refreshToken). 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() return newToken, nil
if err != nil {
log.Error().Err(err).Msg("authenticate/providers.RefreshAccessToken")
return "", 0, err
}
return newToken.AccessToken, time.Until(newToken.Expiry), nil
} }
// Revoke enables a user to revoke her token. If the identity provider supports revocation // Revoke enables a user to revoke her token. If the identity provider supports revocation
// the endpoint is available, otherwise an error is thrown. // 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") 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" "net/http"
"os" "os"
"google.golang.org/grpc"
"github.com/pomerium/pomerium/authenticate" "github.com/pomerium/pomerium/authenticate"
"github.com/pomerium/pomerium/internal/https" "github.com/pomerium/pomerium/internal/https"
"github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/options" "github.com/pomerium/pomerium/internal/options"
"github.com/pomerium/pomerium/internal/version" "github.com/pomerium/pomerium/internal/version"
pb "github.com/pomerium/pomerium/proto/authenticate"
"github.com/pomerium/pomerium/proxy" "github.com/pomerium/pomerium/proxy"
) )
@ -34,6 +38,10 @@ func main() {
} }
log.Info().Str("version", version.FullVersion()).Msg("cmd/pomerium") 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 authenticateService *authenticate.Authenticate
var authHost string var authHost string
if mainOpts.Services == "all" || mainOpts.Services == "authenticate" { if mainOpts.Services == "all" || mainOpts.Services == "authenticate" {
@ -51,6 +59,8 @@ func main() {
log.Fatal().Err(err).Msg("cmd/pomerium: new authenticate") log.Fatal().Err(err).Msg("cmd/pomerium: new authenticate")
} }
authHost = authOpts.RedirectURL.Host authHost = authOpts.RedirectURL.Host
pb.RegisterAuthenticatorServer(grpcServer, authenticateService)
} }
var proxyService *proxy.Proxy var proxyService *proxy.Proxy
@ -64,6 +74,7 @@ func main() {
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: new proxy") log.Fatal().Err(err).Msg("cmd/pomerium: new proxy")
} }
defer proxyService.AuthenticateConn.Close()
} }
topMux := http.NewServeMux() topMux := http.NewServeMux()
@ -85,5 +96,7 @@ func main() {
CertFile: mainOpts.CertFile, CertFile: mainOpts.CertFile,
KeyFile: mainOpts.KeyFile, 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" package main // import "github.com/pomerium/pomerium/cmd/pomerium"
import ( import (
"errors"
"fmt" "fmt"
"github.com/pomerium/envconfig" "github.com/pomerium/envconfig"
@ -14,6 +15,11 @@ type Options struct {
// Debug enables more verbose logging, and outputs human-readable logs to Stdout. // Debug enables more verbose logging, and outputs human-readable logs to Stdout.
// Set with POMERIUM_DEBUG // Set with POMERIUM_DEBUG
Debug bool `envconfig:"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. // Services is a list enabled service mode. If none are selected, "all" is used.
// Available options are : "all", "authenticate", "proxy". // Available options are : "all", "authenticate", "proxy".
Services string `envconfig:"SERVICES"` Services string `envconfig:"SERVICES"`
@ -33,7 +39,7 @@ var defaultOptions = &Options{
Services: "all", Services: "all",
} }
// optionsFromEnvConfig builds the authentication service's configuration // optionsFromEnvConfig builds the IdentityProvider service's configuration
// options from provided environmental variables // options from provided environmental variables
func optionsFromEnvConfig() (*Options, error) { func optionsFromEnvConfig() (*Options, error) {
o := defaultOptions o := defaultOptions
@ -43,6 +49,9 @@ func optionsFromEnvConfig() (*Options, error) {
if !isValidService(o.Services) { if !isValidService(o.Services) {
return nil, fmt.Errorf("%s is an invalid service type", 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 return o, nil
} }

View file

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

View file

@ -34,18 +34,29 @@ Uses the [latest pomerium build](https://hub.docker.com/r/pomerium/pomerium) fro
- Minimal container-based configuration. - Minimal container-based configuration.
- Docker and Docker-Compose based. - Docker and Docker-Compose based.
- Uses pre-configured built-in nginx load balancer - Runs a single container for all pomerium services
- Runs separate containers for each service
- Comes with a pre-configured instance of on-prem Gitlab-CE
- Routes default to on-prem [helloworld], [httpbin] containers. - Routes default to on-prem [helloworld], [httpbin] containers.
Customize for your identity provider run `docker-compose up -f basic.docker-compose.yml` Customize for your identity provider run `docker-compose up -f basic.docker-compose.yml`
#### 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. - Docker and Docker-Compose based.
- Uses pre-configured built-in nginx load balancer - 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 #### gitlab.docker-compose.yml
<<< @/docs/docs/examples/gitlab.docker-compose.yml <<< @/docs/docs/examples/docker/gitlab.docker-compose.yml
## Kubernetes ## 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: services:
nginx: nginx:
image: jwilder/nginx-proxy:latest image: pomerium/nginx-proxy:latest
ports: ports:
- "443:443" - "443:443"
volumes: volumes:
@ -17,18 +17,17 @@ services:
pomerium-authenticate: pomerium-authenticate:
build: . build: .
restart: always restart: always
depends_on:
- "gitlab"
environment: environment:
- POMERIUM_DEBUG=true - POMERIUM_DEBUG=true
- SERVICES=authenticate - SERVICES=authenticate
# auth settings # auth settings
- REDIRECT_URL=https://sso-auth.corp.beyondperimeter.com/oauth2/callback - REDIRECT_URL=https://auth.corp.beyondperimeter.com/oauth2/callback
- IDP_PROVIDER="gitlab" # Identity Provider Settings (Must be changed!)
- IDP_PROVIDER_URL=https://gitlab.corp.beyondperimeter.com - IDP_PROVIDER=google
- IDP_CLIENT_ID=022dbbd09402441dc7af1924b679bc5e6f5bf0d7a555e55b38c51e2e4e6cee76 - IDP_PROVIDER_URL=https://accounts.google.com
- IDP_CLIENT_SECRET=fb7598c520c346915ee369eee57688938fe4f31329a308c4669074da562714b2 - IDP_CLIENT_ID=REPLACEME
- PROXY_ROOT_DOMAIN=beyondperimeter.com - IDP_CLIENT_SECRET=REPLACE_ME
- PROXY_ROOT_DOMAIN=corp.beyondperimeter.com
- ALLOWED_DOMAINS=* - ALLOWED_DOMAINS=*
- SKIP_PROVIDER_BUTTON=false - SKIP_PROVIDER_BUTTON=false
# shared service settings # shared service settings
@ -36,14 +35,13 @@ services:
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M= - SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI= - COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
- VIRTUAL_PROTO=https - VIRTUAL_PROTO=https
- VIRTUAL_HOST=sso-auth.corp.beyondperimeter.com - VIRTUAL_HOST=auth.corp.beyondperimeter.com
- VIRTUAL_PORT=443 - VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files volumes: # volumes is optional; used if passing certificates as files
- ./cert.pem:/pomerium/cert.pem:ro - ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro - ./privkey.pem:/pomerium/privkey.pem:ro
expose: expose:
- 443 - 443
pomerium-proxy: pomerium-proxy:
build: . build: .
restart: always restart: always
@ -51,12 +49,17 @@ services:
- POMERIUM_DEBUG=true - POMERIUM_DEBUG=true
- SERVICES=proxy - SERVICES=proxy
# proxy settings # proxy settings
- AUTHENTICATE_SERVICE_URL=https://sso-auth.corp.beyondperimeter.com - AUTHENTICATE_SERVICE_URL=https://auth.corp.beyondperimeter.com
- ROUTES=https://httpbin.corp.beyondperimeter.com=http://httpbin,https://hello.corp.beyondperimeter.com=http://hello-world/ # 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` # Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M= - SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI= - COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
- SIGNING_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
# nginx settings # nginx settings
- VIRTUAL_PROTO=https - VIRTUAL_PROTO=https
- VIRTUAL_HOST=*.corp.beyondperimeter.com - 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 app: pomerium-authenticate
spec: spec:
containers: containers:
- image: pomerium/pomerium:latest - image: pomerium/pomerium:grpctest
name: pomerium-authenticate name: pomerium-authenticate
ports: ports:
- containerPort: 443 - containerPort: 443
@ -26,7 +26,7 @@ spec:
- name: SERVICES - name: SERVICES
value: authenticate value: authenticate
- name: REDIRECT_URL - name: REDIRECT_URL
value: https://sso-auth.corp.beyondperimeter.com/oauth2/callback value: https://auth.corp.beyondperimeter.com/oauth2/callback
- name: IDP_PROVIDER - name: IDP_PROVIDER
value: google value: google
- name: IDP_PROVIDER_URL - name: IDP_PROVIDER_URL
@ -62,12 +62,6 @@ spec:
secretKeyRef: secretKeyRef:
name: certificate-key name: certificate-key
key: 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: readinessProbe:
httpGet: httpGet:
path: /ping 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 - secretName: pomerium-tls
hosts: hosts:
- "*.corp.beyondperimeter.com" - "*.corp.beyondperimeter.com"
- "sso-auth.corp.beyondperimeter.com" - "auth.corp.beyondperimeter.com"
rules: rules:
- host: "*.corp.beyondperimeter.com" - host: "*.corp.beyondperimeter.com"
http: http:
paths: paths:
- path: / - paths:
backend: backend:
serviceName: pomerium-proxy-service serviceName: pomerium-proxy-service
servicePort: 443 servicePort: https
- path: /*
backend:
serviceName: pomerium-proxy-service
servicePort: 443
- host: "sso-auth.corp.beyondperimeter.com" - host: "auth.corp.beyondperimeter.com"
http: http:
paths: paths:
- path: /* - paths:
backend: backend:
serviceName: pomerium-authenticate-service serviceName: pomerium-authenticate-service
servicePort: 443 servicePort: https
- path: /
backend:
serviceName: pomerium-authenticate-service
servicePort: 443

View file

@ -16,7 +16,7 @@ spec:
app: pomerium-proxy app: pomerium-proxy
spec: spec:
containers: containers:
- image: pomerium/pomerium:latest - image: pomerium/pomerium:grpctest
name: pomerium-proxy name: pomerium-proxy
ports: ports:
- containerPort: 443 - containerPort: 443
@ -24,11 +24,15 @@ spec:
protocol: TCP protocol: TCP
env: env:
- name: ROUTES - 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 - name: SERVICES
value: proxy value: proxy
- name: AUTHENTICATE_SERVICE_URL - 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 - name: SHARED_SECRET
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
@ -54,12 +58,6 @@ spec:
secretKeyRef: secretKeyRef:
name: certificate-key name: certificate-key
key: certificate-key key: certificate-key
- name: VIRTUAL_PROTO
value: https
- name: VIRTUAL_HOST
value: "*.corp.beyondperimeter.com"
- name: VIRTUAL_PORT
value: "443"
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /ping 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. 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**. 2. Generate a **Client ID** and **Client Secret**.
3. Configure pomerium to use the **Client ID** and **Client Secret** keys. 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 1. Provision a new cluster
2. Create authenticate and proxy [deployments](https://cloud.google.com/kubernetes-engine/docs/concepts/deployment). 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). 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 ```bash
sh ./scripts/kubernetes_gke.sh 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 ## 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 ```bash
docker-compose up docker-compose up

5
go.mod
View file

@ -3,6 +3,8 @@ module github.com/pomerium/pomerium
require ( require (
github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3 github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3
github.com/davecgh/go-spew v1.1.1 // indirect 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/pmezard/go-difflib v1.0.0 // indirect
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31 github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31
github.com/pomerium/go-oidc v2.0.0+incompatible github.com/pomerium/go-oidc v2.0.0+incompatible
@ -10,10 +12,11 @@ require (
github.com/rs/zerolog v1.11.0 github.com/rs/zerolog v1.11.0
github.com/stretchr/testify v1.2.2 // indirect github.com/stretchr/testify v1.2.2 // indirect
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 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/oauth2 v0.0.0-20181203162652-d668ce993890
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 // indirect golang.org/x/sys v0.0.0-20190116161447-11f53e031339 // indirect
google.golang.org/appengine v1.4.0 // 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 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 h1:wOysYcIdqv3WnvwqFFzrYCFALPED7qkUGaLXu359GSc=
github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3/go.mod h1:UMqtWQTnOe4byzwe7Zhwh8f8s+36uszN51sJrSIZlTE= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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= 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= 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 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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-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 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-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 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0=
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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/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 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 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 h1:uRIz/V7RfMsMgGnCp+YybIdstDIz8wc0H283wHQfwic=
gopkg.in/square/go-jose.v2 v2.2.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 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 apiVersion: v1
appVersion: 0.0.1 appVersion: 0.0.1
home: http://www.pomerium.io/ 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: keywords:
- kubernetes - kubernetes
- oauth - oauth
- oauth2 - oauth2
- authentication - IdentityProvider
- google - google
- okta - okta
- azure - azure

View file

@ -24,7 +24,7 @@ proxy:
# For any other settings that are optional # For any other settings that are optional
# ADDRESS, POMERIUM_DEBUG, CERTIFICATE_FILE, CERTIFICATE_KEY_FILE # 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 # DEFAULT_UPSTREAM_TIMEOUT, PASS_ACCESS_TOKEN, SESSION_VALID_TTL, SESSION_LIFETIME_TTL, GRACE_PERIOD_TTL
extraEnv: {} extraEnv: {}

View file

@ -38,7 +38,7 @@ type XChaCha20Cipher struct {
aead cipher.AEAD 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) { func NewCipher(secret []byte) (*XChaCha20Cipher, error) {
aead, err := chacha20poly1305.NewX(secret) aead, err := chacha20poly1305.NewX(secret)
if err != nil { if err != nil {

View file

@ -8,9 +8,11 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/pomerium/pomerium/internal/fileutil" "github.com/pomerium/pomerium/internal/fileutil"
"google.golang.org/grpc"
) )
// Options contains the configurations settings for a TLS http server. // 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 // ListenAndServeTLS serves the provided handlers by HTTPS
// using the provided options. // 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 { if opt == nil {
opt = defaultOptions opt = defaultOptions
} else { } else {
@ -82,16 +84,21 @@ func ListenAndServeTLS(opt *Options, handler http.Handler) error {
ln = tls.NewListener(ln, config) ln = tls.NewListener(ln, config)
var h http.Handler
if grpcHandler == nil {
h = httpHandler
} else {
h = grpcHandlerFunc(grpcHandler, httpHandler)
}
// Set up the main server. // Set up the main server.
server := &http.Server{ server := &http.Server{
ReadHeaderTimeout: 5 * time.Second, ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second, ReadTimeout: 10 * time.Second,
// WriteTimeout is set to 0 because it also pertains to // WriteTimeout is set to 0 for streaming replies
// streaming replies, e.g., the DirServer.Watch interface.
WriteTimeout: 0, WriteTimeout: 0,
IdleTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second,
TLSConfig: config, TLSConfig: config,
Handler: handler, Handler: h,
} }
return server.Serve(ln) 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. // 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://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) { func newDefaultTLSConfig(cert *tls.Certificate) (*tls.Config, error) {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
// Prioritize cipher suites sped up by AES-NI (AES-GCM)
CipherSuites: []uint16{ CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 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_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
}, },
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true, 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() tlsConfig.BuildNameToCertificate()
return tlsConfig, nil 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/http"
"net/url" "net/url"
"time" "time"
"github.com/pomerium/pomerium/internal/log"
) )
// ErrTokenRevoked signifies a token revokation or expiration error // 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 { if err != nil {
return err return err
} }
log.Info().Msgf("%s", respBody)
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
switch resp.StatusCode { switch resp.StatusCode {

View file

@ -10,11 +10,12 @@ import (
) )
// Logger is the global logger. // 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. // SetDebugMode tells the logger to use standard out and pretty print output.
func SetDebugMode() { func SetDebugMode() {
Logger = Logger.Output(zerolog.ConsoleWriter{Out: os.Stdout}) Logger = Logger.Output(zerolog.ConsoleWriter{Out: os.Stdout})
// zerolog.SetGlobalLevel(zerolog.InfoLevel)
} }
// With creates a child logger with the field added to its context. // 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 func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, ok := mux[r.Host]; !ok { 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 return
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)

View file

@ -16,15 +16,16 @@ var (
type SessionState struct { type SessionState struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_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"` RefreshDeadline time.Time `json:"refresh_deadline"`
LifetimeDeadline time.Time `json:"lifetime_deadline"` LifetimeDeadline time.Time `json:"lifetime_deadline"`
ValidDeadline time.Time `json:"valid_deadline"` ValidDeadline time.Time `json:"valid_deadline"`
GracePeriodStart time.Time `json:"grace_period_start"` GracePeriodStart time.Time `json:"grace_period_start"`
Email string `json:"email"` Email string `json:"email"`
User string `json:"user"` User string `json:"user"` // 'sub' in jwt parlance
Groups []string `json:"groups"`
} }
// LifetimePeriodExpired returns true if the lifetime has expired // LifetimePeriodExpired returns true if the lifetime has expired

View file

@ -2,11 +2,12 @@ package templates // import "github.com/pomerium/pomerium/internal/templates"
import ( import (
"testing" "testing"
"github.com/pomerium/pomerium/internal/testutil"
) )
func TestTemplatesCompile(t *testing.T) { func TestTemplatesCompile(t *testing.T) {
templates := New() 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" package proxy // import "github.com/pomerium/pomerium/proxy"
import ( import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -17,7 +20,7 @@ import (
) )
var ( 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") ErrUserNotAuthorized = errors.New("user not authorized")
) )
@ -27,6 +30,12 @@ var securityHeaders = map[string]string{
"X-XSS-Protection": "1; mode=block", "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 // Handler returns a http handler for an Proxy
func (p *Proxy) Handler() http.Handler { func (p *Proxy) Handler() http.Handler {
// routes // routes
@ -61,6 +70,8 @@ func (p *Proxy) Handler() http.Handler {
c = c.Append(middleware.RefererHandler("referer")) c = c.Append(middleware.RefererHandler("referer"))
c = c.Append(middleware.RequestIDHandler("req_id", "Request-Id")) c = c.Append(middleware.RequestIDHandler("req_id", "Request-Id"))
c = c.Append(middleware.ValidateHost(p.mux)) c = c.Append(middleware.ValidateHost(p.mux))
// serve the middleware and mux
h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mux.ServeHTTP(w, r) 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 // 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. // automatically requests the favicon on an error page.
func (p *Proxy) Favicon(w http.ResponseWriter, r *http.Request) { func (p *Proxy) Favicon(w http.ResponseWriter, r *http.Request) {
err := p.Authenticate(w, r) err := p.Authenticate(w, r)
@ -93,15 +104,13 @@ func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
Host: r.Host, Host: r.Host,
Path: "/", Path: "/",
} }
fullURL := p.authenticateClient.GetSignOutURL(redirectURL) fullURL := p.GetSignOutURL(p.AuthenticateURL, redirectURL)
http.Redirect(w, r, fullURL.String(), http.StatusFound) 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. // in a request to the provider's sign in endpoint.
func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) { 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() requestURI := r.URL.String()
callbackURL := p.GetRedirectURL(r.Host) 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) httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
return return
} }
signinURL := p.GetSignInURL(p.AuthenticateURL, callbackURL, encryptedState)
signinURL := p.authenticateClient.GetSignInURL(callbackURL, encryptedState) log.FromRequest(r).Info().
log.FromRequest(r).Info().Msg("redirecting to begin auth flow") 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) http.Redirect(w, r, signinURL.String(), http.StatusFound)
} }
// OAuthCallback validates the cookie sent back from the provider, then validates he user // OAuthCallback validates the cookie sent back from the authenticate service. This function will
// information, and if authorized, redirects the user back to the original application. // 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) { 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() err := r.ParseForm()
if err != nil { if err != nil {
log.FromRequest(r).Error().Err(err).Msg("failed parsing request form") 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) httputil.ErrorResponse(w, r, errorString, http.StatusForbidden)
return return
} }
// We begin the process of redeeming the code for an access token. // 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 { if err != nil {
log.FromRequest(r).Error().Err(err).Msg("error redeeming authorization code") log.FromRequest(r).Error().Err(err).Msg("error redeeming authorization code")
httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError) httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError)
@ -208,6 +217,14 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
return 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 // This is the redirect back to the original requested application
http.Redirect(w, r, stateParameter.RedirectURI, http.StatusFound) 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, // 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) { func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
// Attempts to validate the user and their cookie.
err := p.Authenticate(w, r) 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. // OAuthStart. If successful, we proceed to proxy to the configured upstream.
if err != nil { if err != nil {
switch err { switch err {
case ErrUserNotAuthorized: case ErrUserNotAuthorized:
//todo(bdd) : custom forbidden page with details and troubleshooting info
log.FromRequest(r).Debug().Err(err).Msg("proxy: user access forbidden") log.FromRequest(r).Debug().Err(err).Msg("proxy: user access forbidden")
httputil.ErrorResponse(w, r, "You don't have access", http.StatusForbidden) httputil.ErrorResponse(w, r, "You don't have access", http.StatusForbidden)
return return
@ -245,6 +260,9 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
// ! ! !
// todo(bdd): ! Authorization checks will go here !
// ! ! !
// We have validated the users request and now proxy their request to the provided upstream. // We have validated the users request and now proxy their request to the provided upstream.
route, ok := p.router(r) route, ok := p.router(r)
@ -270,29 +288,38 @@ func (p *Proxy) Authenticate(w http.ResponseWriter, r *http.Request) (err error)
if err != nil { if err != nil {
return err return err
} }
if session.LifetimePeriodExpired() { if session.LifetimePeriodExpired() {
log.FromRequest(r).Info().Msg("proxy.Authenticate: lifetime expired, restarting")
return sessions.ErrLifetimeExpired 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 { if err != nil {
log.FromRequest(r).Warn().
Str("RefreshToken", session.RefreshToken).
Str("AccessToken", session.AccessToken).
Msg("proxy.Authenticate: refresh failure")
return err return err
} }
if !ok { session.AccessToken = accessToken
return ErrUserNotAuthorized session.RefreshDeadline = expiry
} log.FromRequest(r).Info().
} else if session.ValidationPeriodExpired() { Str("RefreshToken", session.RefreshToken).
ok := p.authenticateClient.ValidateSessionState(session) Str("AccessToken", session.AccessToken).
if !ok { Msg("proxy.Authenticate: refresh success")
return ErrUserNotAuthorized
}
} }
err = p.sessionStore.SaveSession(w, r, session) err = p.sessionStore.SaveSession(w, r, session)
if err != nil { if err != nil {
return err return err
} }
// pass user & user-email details to client applications
r.Header.Set(HeaderUserID, session.User) r.Header.Set(HeaderUserID, session.User)
r.Header.Set(HeaderEmail, session.Email) r.Header.Set(HeaderEmail, session.Email)
// This user has been OK'd. Allow the request! // This user has been OK'd. Allow the request!
return nil 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, // GetRedirectURL returns the redirect url for a given Proxy,
// setting the scheme to be https if CookieSecure is true. // setting the scheme to be https if CookieSecure is true.
func (p *Proxy) GetRedirectURL(host string) *url.URL { func (p *Proxy) GetRedirectURL(host string) *url.URL {
// TODO: Ensure that we only allow valid upstream hosts in redirect URIs
u := p.redirectURL u := p.redirectURL
// Build redirect URI from request host // Build redirect URI from request host
if u.Scheme == "" { if u.Scheme == "" {
@ -326,19 +352,39 @@ func (p *Proxy) GetRedirectURL(host string) *url.URL {
return u return u
} }
func (p *Proxy) redeemCode(host, code string) (*sessions.SessionState, error) { // signRedirectURL signs the redirect url string, given a timestamp, and returns it
if code == "" { func (p *Proxy) signRedirectURL(rawRedirect string, timestamp time.Time) string {
return nil, errors.New("missing code") h := hmac.New(sha256.New, []byte(p.SharedKey))
} h.Write([]byte(rawRedirect))
redirectURL := p.GetRedirectURL(host) h.Write([]byte(fmt.Sprint(timestamp.Unix())))
s, err := p.authenticateClient.Redeem(redirectURL.String(), code) return base64.URLEncoding.EncodeToString(h.Sum(nil))
if err != nil { }
return s, err
} // GetSignInURL with typical oauth parameters
func (p *Proxy) GetSignInURL(authenticateURL, redirectURL *url.URL, state string) *url.URL {
if s.Email == "" { a := authenticateURL.ResolveReference(&url.URL{Path: "/sign_in"})
return s, errors.New("invalid email address") now := time.Now()
} rawRedirect := redirectURL.String()
params, _ := url.ParseQuery(a.RawQuery)
return s, nil 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" package proxy // import "github.com/pomerium/pomerium/proxy"
import ( import (
"crypto/tls"
"crypto/x509"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
@ -13,11 +15,15 @@ import (
"time" "time"
"github.com/pomerium/envconfig" "github.com/pomerium/envconfig"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"github.com/pomerium/pomerium/internal/cryptutil" "github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/templates" "github.com/pomerium/pomerium/internal/templates"
"github.com/pomerium/pomerium/proxy/authenticator" pb "github.com/pomerium/pomerium/proto/authenticate"
) )
const ( const (
@ -29,10 +35,13 @@ const (
HeaderEmail = "x-pomerium-authenticated-user-email" 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 { type Options struct {
// AuthenticateServiceURL specifies the url to the pomerium authenticate http service. // Authenticate service settings
AuthenticateServiceURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"` 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. // 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 // 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 is a 32 byte random key used to authenticate access between services.
SharedKey string `envconfig:"SHARED_SECRET"` 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"` // Sub-routes
CookieSecret string `envconfig:"COOKIE_SECRET"` Routes map[string]string `envconfig:"ROUTES"`
CookieDomain string `envconfig:"COOKIE_DOMAIN"` DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"`
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"`
} }
// NewOptions returns a new options struct // NewOptions returns a new options struct
var defaultOptions = &Options{ var defaultOptions = &Options{
CookieName: "_pomerium_proxy", CookieName: "_pomerium_proxy",
CookieHTTPOnly: false, CookieHTTPOnly: true,
CookieSecure: true,
CookieExpire: time.Duration(168) * time.Hour, CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(30) * time.Minute,
CookieLifetimeTTL: time.Duration(720) * time.Hour,
DefaultUpstreamTimeout: time.Duration(10) * time.Second, 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 // options from provided environmental variables
func OptionsFromEnvConfig() (*Options, error) { func OptionsFromEnvConfig() (*Options, error) {
o := defaultOptions o := defaultOptions
@ -94,11 +99,11 @@ func (o *Options) Validate() error {
return fmt.Errorf("could not parse destination %s as url : %q", to, err) return fmt.Errorf("could not parse destination %s as url : %q", to, err)
} }
} }
if o.AuthenticateServiceURL == nil { if o.AuthenticateURL == nil {
return errors.New("missing setting: provider-url") return errors.New("missing setting: authenticate-service-url")
} }
if o.AuthenticateServiceURL.Scheme != "https" { if o.AuthenticateURL.Scheme != "https" {
return errors.New("provider-url must be a valid https url") return errors.New("authenticate-service-url must be a valid https url")
} }
if o.CookieSecret == "" { if o.CookieSecret == "" {
return errors.New("missing setting: cookie-secret") 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. // Proxy stores all the information associated with proxying a request.
type Proxy struct { type Proxy struct {
PassAccessToken bool SharedKey string
// services // Authenticate Service Configuration
authenticateClient *authenticator.AuthenticateClient AuthenticateURL *url.URL
AuthenticateInternalURL string
AuthenticatorClient pb.AuthenticatorClient
// AuthenticateConn must be closed by Proxy's caller
AuthenticateConn *grpc.ClientConn
OverideCertificateName string
// session // session
cipher cryptutil.Cipher cipher cryptutil.Cipher
csrfStore sessions.CSRFStore csrfStore sessions.CSRFStore
sessionStore sessions.SessionStore sessionStore sessions.SessionStore
CookieExpire time.Duration
CookieRefresh time.Duration
CookieLifetimeTTL time.Duration
redirectURL *url.URL redirectURL *url.URL
templates *template.Template templates *template.Template
mux map[string]http.Handler 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. // New takes a Proxy service from options and a validation function.
// Function returns an error if options fail to validate. // Function returns an error if options fail to validate.
func New(opts *Options) (*Proxy, error) { func New(opts *Options) (*Proxy, error) {
@ -173,27 +181,21 @@ func New(opts *Options) (*Proxy, error) {
return nil, err 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{ p := &Proxy{
// these fields make up the routing mechanism // these fields make up the routing mechanism
mux: make(map[string]http.Handler), mux: make(map[string]http.Handler),
// session state // session state
cipher: cipher, cipher: cipher,
csrfStore: cookieStore, csrfStore: cookieStore,
sessionStore: cookieStore, sessionStore: cookieStore,
AuthenticateURL: opts.AuthenticateURL,
authenticateClient: authClient, AuthenticateInternalURL: opts.AuthenticateInternalURL,
redirectURL: &url.URL{Path: "/.pomerium/callback"}, OverideCertificateName: opts.OverideCertificateName,
templates: templates.New(), SharedKey: opts.SharedKey,
PassAccessToken: opts.PassAccessToken, redirectURL: &url.URL{Path: "/.pomerium/callback"},
templates: templates.New(),
CookieExpire: opts.CookieExpire,
CookieLifetimeTTL: opts.CookieLifetimeTTL,
} }
for from, to := range opts.Routes { for from, to := range opts.Routes {
@ -205,8 +207,42 @@ func New(opts *Options) (*Proxy, error) {
return nil, err return nil, err
} }
p.Handle(fromURL.Host, handler) 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 return p, nil
} }
@ -260,8 +296,8 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
u.handler.ServeHTTP(w, r) u.handler.ServeHTTP(w, r)
} }
// NewReverseProxy creates a reverse proxy to a specified url. // NewReverseProxy returns a new ReverseProxy that routes URLs to the scheme, host, and
// It adds an X-Forwarded-Host header that is the request's host. // base path provided in target. NewReverseProxy rewrites the Host header.
func NewReverseProxy(to *url.URL) *httputil.ReverseProxy { func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
proxy := httputil.NewSingleHostReverseProxy(to) proxy := httputil.NewSingleHostReverseProxy(to)
proxy.Transport = defaultUpstreamTransport proxy.Transport = defaultUpstreamTransport

View file

@ -11,10 +11,9 @@ import (
"testing" "testing"
) )
func init() {
os.Clearenv()
}
func TestOptionsFromEnvConfig(t *testing.T) { func TestOptionsFromEnvConfig(t *testing.T) {
os.Clearenv()
tests := []struct { tests := []struct {
name string name string
want *Options want *Options
@ -23,9 +22,9 @@ func TestOptionsFromEnvConfig(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{"good default, no env settings", defaultOptions, "", "", false}, {"good default, no env settings", defaultOptions, "", "", false},
{"bad url", nil, "AUTHENTICATE_SERVICE_URL", "%.rjlw", true}, {"bad url", nil, "AUTHENTICATE_SERVICE_URL", "%.ugly", true},
{"good duration", defaultOptions, "SESSION_VALID_TTL", "1m", false}, {"good duration", defaultOptions, "COOKIE_REFRESH", "1m", false},
{"bad duration", nil, "SESSION_VALID_TTL", "1sm", true}, {"bad duration", nil, "COOKIE_REFRESH", "1sm", true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -46,6 +45,8 @@ func TestOptionsFromEnvConfig(t *testing.T) {
} }
func Test_urlParse(t *testing.T) { func Test_urlParse(t *testing.T) {
os.Clearenv()
tests := []struct { tests := []struct {
name string name string
uri string uri string
@ -131,10 +132,10 @@ func TestNewReverseProxyHandler(t *testing.T) {
func testOptions() *Options { func testOptions() *Options {
authurl, _ := url.Parse("https://sso-auth.corp.beyondperimeter.com") authurl, _ := url.Parse("https://sso-auth.corp.beyondperimeter.com")
return &Options{ return &Options{
Routes: map[string]string{"corp.example.com": "example.com"}, Routes: map[string]string{"corp.example.com": "example.com"},
AuthenticateServiceURL: authurl, AuthenticateURL: authurl,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=", SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=", CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
} }
} }
@ -145,10 +146,10 @@ func TestOptions_Validate(t *testing.T) {
badToRoute := testOptions() badToRoute := testOptions()
badToRoute.Routes = map[string]string{"^": "example.com"} badToRoute.Routes = map[string]string{"^": "example.com"}
badAuthURL := testOptions() badAuthURL := testOptions()
badAuthURL.AuthenticateServiceURL = nil badAuthURL.AuthenticateURL = nil
authurl, _ := url.Parse("http://sso-auth.corp.beyondperimeter.com") authurl, _ := url.Parse("http://sso-auth.corp.beyondperimeter.com")
httpAuthURL := testOptions() httpAuthURL := testOptions()
httpAuthURL.AuthenticateServiceURL = authurl httpAuthURL.AuthenticateURL = authurl
emptyCookieSecret := testOptions() emptyCookieSecret := testOptions()
emptyCookieSecret.CookieSecret = "" emptyCookieSecret.CookieSecret = ""
invalidCookieSecret := testOptions() 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. # resources to avoid being billed. For reference, this tutorial cost me <10 cents for a couple of hours.
# create a cluster # 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 # get cluster credentials os we can use kubctl locally
gcloud container clusters get-credentials pomerium gcloud container clusters get-credentials pomerium
# create `pomerium` namespace # create `pomerium` namespace
kubectl create ns pomerium 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 # 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 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) 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 !!! # !!! IMPORTANT !!!
# YOU MUST CHANGE THE Identity Provider Client Secret # YOU MUST CHANGE THE Identity Provider Client Secret
# !!! IMPORTANT !!! # !!! 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 # Create the proxy & authenticate deployment
kubectl create -f docs/docs/examples/kubernetes/authenticate.deploy.yml kubectl apply -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/proxy.deploy.yml
# Create the proxy & authenticate services # Create the proxy & authenticate services
kubectl apply -f docs/docs/examples/kubernetes/proxy.service.yml kubectl apply -f docs/docs/examples/kubernetes/proxy.service.yml
kubectl apply -f docs/docs/examples/kubernetes/authenticate.service.yml kubectl apply -f docs/docs/examples/kubernetes/authenticate.service.yml
# Create and apply the Ingress; this is GKE specific # Create and apply the Ingress; this is GKE specific
kubectl apply -f docs/docs/examples/kubernetes/ingress.yml 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! # When done, clean up by deleting the cluster!
# gcloud container clusters delete pomerium # gcloud container clusters delete pomerium