mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-30 19:06:33 +02:00
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:
parent
9ca3ff4fa2
commit
c886b924e7
54 changed files with 2184 additions and 1463 deletions
24
3RD-PARTY
24
3RD-PARTY
|
@ -105,6 +105,30 @@ https://github.com/justinas/alice/blob/master/LICENSE
|
|||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
goji
|
||||
SPDX-License-Identifier: MIT
|
||||
https://github.com/zenazn/goji/blob/master/LICENSE
|
||||
|
||||
Copyright (c) 2014, 2015, 2016 Carl Jackson (carl@avtok.com)
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
|
|
|
@ -18,18 +18,19 @@ import (
|
|||
)
|
||||
|
||||
var defaultOptions = &Options{
|
||||
CookieName: "_pomerium_authenticate",
|
||||
CookieHTTPOnly: true,
|
||||
CookieExpire: time.Duration(168) * time.Hour,
|
||||
CookieRefresh: time.Duration(1) * time.Hour,
|
||||
SessionLifetimeTTL: time.Duration(720) * time.Hour,
|
||||
Scopes: []string{"openid", "email", "profile"},
|
||||
CookieName: "_pomerium_authenticate",
|
||||
CookieHTTPOnly: true,
|
||||
CookieSecure: true,
|
||||
CookieExpire: time.Duration(168) * time.Hour,
|
||||
CookieRefresh: time.Duration(30) * time.Minute,
|
||||
CookieLifetimeTTL: time.Duration(720) * time.Hour,
|
||||
}
|
||||
|
||||
// Options permits the configuration of the authentication service
|
||||
// Options details the available configuration settings for the authenticate service
|
||||
type Options struct {
|
||||
RedirectURL *url.URL `envconfig:"REDIRECT_URL"`
|
||||
|
||||
// SharedKey is used to authenticate requests between services
|
||||
SharedKey string `envconfig:"SHARED_SECRET"`
|
||||
|
||||
// Coarse authorization based on user email domain
|
||||
|
@ -37,27 +38,25 @@ type Options struct {
|
|||
ProxyRootDomains []string `envconfig:"PROXY_ROOT_DOMAIN"`
|
||||
|
||||
// Session/Cookie management
|
||||
CookieName string
|
||||
CookieSecret string `envconfig:"COOKIE_SECRET"`
|
||||
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
|
||||
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
|
||||
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
|
||||
CookieSecure bool `envconfig:"COOKIE_SECURE"`
|
||||
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
|
||||
CookieName string
|
||||
CookieSecret string `envconfig:"COOKIE_SECRET"`
|
||||
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
|
||||
CookieSecure bool `envconfig:"COOKIE_SECURE"`
|
||||
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
|
||||
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
|
||||
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
|
||||
CookieLifetimeTTL time.Duration `envconfig:"COOKIE_LIFETIME"`
|
||||
|
||||
SessionLifetimeTTL time.Duration `envconfig:"SESSION_LIFETIME_TTL"`
|
||||
|
||||
// Authentication provider configuration variables as specified by RFC6749
|
||||
// IdentityProvider provider configuration variables as specified by RFC6749
|
||||
// See: https://openid.net/specs/openid-connect-basic-1_0.html#RFC6749
|
||||
ClientID string `envconfig:"IDP_CLIENT_ID"`
|
||||
ClientSecret string `envconfig:"IDP_CLIENT_SECRET"`
|
||||
Provider string `envconfig:"IDP_PROVIDER"`
|
||||
ProviderURL string `envconfig:"IDP_PROVIDER_URL"`
|
||||
Scopes []string `envconfig:"IDP_SCOPE"`
|
||||
Scopes []string `envconfig:"IDP_SCOPES"`
|
||||
}
|
||||
|
||||
// OptionsFromEnvConfig builds the authentication service's configuration
|
||||
// options from provided environmental variables
|
||||
// OptionsFromEnvConfig builds the authenticate service's configuration environmental variables
|
||||
func OptionsFromEnvConfig() (*Options, error) {
|
||||
o := defaultOptions
|
||||
if err := envconfig.Process("", o); err != nil {
|
||||
|
@ -66,7 +65,7 @@ func OptionsFromEnvConfig() (*Options, error) {
|
|||
return o, nil
|
||||
}
|
||||
|
||||
// Validate checks to see if configuration values are valid for the authentication service.
|
||||
// Validate checks to see if configuration values are valid for the authenticate service.
|
||||
// The checks do not modify the internal state of the Option structure. Returns
|
||||
// on first error found.
|
||||
func (o *Options) Validate() error {
|
||||
|
@ -102,8 +101,7 @@ func (o *Options) Validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Authenticate is service for validating user authentication for proxied-requests
|
||||
// against third-party identity provider (IdP) services.
|
||||
// Authenticate validates a user's identity
|
||||
type Authenticate struct {
|
||||
RedirectURL *url.URL
|
||||
|
||||
|
@ -115,7 +113,7 @@ type Authenticate struct {
|
|||
|
||||
SharedKey string
|
||||
|
||||
SessionLifetimeTTL time.Duration
|
||||
CookieLifetimeTTL time.Duration
|
||||
|
||||
templates *template.Template
|
||||
csrfStore sessions.CSRFStore
|
||||
|
@ -125,7 +123,7 @@ type Authenticate struct {
|
|||
provider providers.Provider
|
||||
}
|
||||
|
||||
// New validates and creates a new authentication service from a configuration options.
|
||||
// New validates and creates a new authenticate service from a set of Options
|
||||
func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate, error) {
|
||||
if opts == nil {
|
||||
return nil, errors.New("options cannot be nil")
|
||||
|
@ -133,15 +131,9 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
|
|||
if err := opts.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decodedAuthCodeSecret, err := base64.StdEncoding.DecodeString(opts.CookieSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cipher, err := cryptutil.NewCipher([]byte(decodedAuthCodeSecret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decodedCookieSecret, err := base64.StdEncoding.DecodeString(opts.CookieSecret)
|
||||
// checked by validate
|
||||
decodedCookieSecret, _ := base64.StdEncoding.DecodeString(opts.CookieSecret)
|
||||
cipher, err := cryptutil.NewCipher([]byte(decodedCookieSecret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -183,25 +175,22 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func newProvider(opts *Options) (providers.Provider, error) {
|
||||
pd := &providers.ProviderData{
|
||||
pd := &providers.IdentityProvider{
|
||||
RedirectURL: opts.RedirectURL,
|
||||
ProviderName: opts.Provider,
|
||||
ProviderURL: opts.ProviderURL,
|
||||
ClientID: opts.ClientID,
|
||||
ClientSecret: opts.ClientSecret,
|
||||
SessionLifetimeTTL: opts.SessionLifetimeTTL,
|
||||
SessionLifetimeTTL: opts.CookieLifetimeTTL,
|
||||
Scopes: opts.Scopes,
|
||||
}
|
||||
np, err := providers.New(opts.Provider, pd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return providers.NewSingleFlightProvider(np), nil
|
||||
|
||||
return np, err
|
||||
}
|
||||
|
||||
func dotPrependDomains(d []string) []string {
|
||||
|
|
|
@ -8,23 +8,19 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
os.Clearenv()
|
||||
}
|
||||
|
||||
func testOptions() *Options {
|
||||
redirectURL, _ := url.Parse("https://example.com/oauth2/callback")
|
||||
return &Options{
|
||||
ProxyRootDomains: []string{"example.com"},
|
||||
AllowedDomains: []string{"example.com"},
|
||||
RedirectURL: redirectURL,
|
||||
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
|
||||
ClientID: "test-client-id",
|
||||
ClientSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
|
||||
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
|
||||
CookieRefresh: time.Duration(1) * time.Hour,
|
||||
SessionLifetimeTTL: time.Duration(720) * time.Hour,
|
||||
CookieExpire: time.Duration(168) * time.Hour,
|
||||
ProxyRootDomains: []string{"example.com"},
|
||||
AllowedDomains: []string{"example.com"},
|
||||
RedirectURL: redirectURL,
|
||||
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
|
||||
ClientID: "test-client-id",
|
||||
ClientSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
|
||||
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
|
||||
CookieRefresh: time.Duration(1) * time.Hour,
|
||||
CookieLifetimeTTL: time.Duration(720) * time.Hour,
|
||||
CookieExpire: time.Duration(168) * time.Hour,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,6 +77,8 @@ func TestOptions_Validate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOptionsFromEnvConfig(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
want *Options
|
||||
|
@ -91,7 +89,7 @@ func TestOptionsFromEnvConfig(t *testing.T) {
|
|||
{"good default, no env settings", defaultOptions, "", "", false},
|
||||
{"bad url", nil, "REDIRECT_URL", "%.rjlw", true},
|
||||
{"good duration", defaultOptions, "COOKIE_EXPIRE", "1m", false},
|
||||
{"bad duration", nil, "COOKIE_EXPIRE", "1sm", true},
|
||||
{"bad duration", nil, "COOKIE_REFRESH", "1sm", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -131,3 +129,65 @@ func Test_dotPrependDomains(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newProvider(t *testing.T) {
|
||||
redirectURL, _ := url.Parse("https://example.com/oauth3/callback")
|
||||
|
||||
goodOpts := &Options{
|
||||
RedirectURL: redirectURL,
|
||||
Provider: "google",
|
||||
ProviderURL: "",
|
||||
ClientID: "cllient-id",
|
||||
ClientSecret: "client-secret",
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *Options
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", goodOpts, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := newProvider(tt.opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("newProvider() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
// if !reflect.DeepEqual(got, tt.want) {
|
||||
// t.Errorf("newProvider() = %v, want %v", got, tt.want)
|
||||
// }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
good := testOptions()
|
||||
good.Provider = "google"
|
||||
|
||||
badRedirectURL := testOptions()
|
||||
badRedirectURL.RedirectURL = nil
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *Options
|
||||
// want *Authenticate
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", good, false},
|
||||
{"empty opts", nil, true},
|
||||
{"fails to validate", badRedirectURL, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := New(tt.opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
// if !reflect.DeepEqual(got, tt.want) {
|
||||
// t.Errorf("New() = %v, want %v", got, tt.want)
|
||||
// }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
64
authenticate/grpc.go
Normal file
64
authenticate/grpc.go
Normal 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
170
authenticate/grpc_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package authenticate // import "github.com/pomerium/pomerium/authenticate"
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -17,16 +16,19 @@ import (
|
|||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
// securityHeaders corresponds to HTTP response headers related to security.
|
||||
// https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Headers
|
||||
var securityHeaders = map[string]string{
|
||||
"Strict-Transport-Security": "max-age=31536000",
|
||||
"X-Frame-Options": "DENY",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-XSS-Protection": "1; mode=block",
|
||||
"Content-Security-Policy": "default-src 'none'; style-src 'self' 'sha256-pSTVzZsFAqd2U3QYu+BoBDtuJWaPM/+qMy/dBRrhb5Y='; img-src 'self';",
|
||||
"Referrer-Policy": "Same-origin",
|
||||
"Content-Security-Policy": "default-src 'none'; style-src 'self' " +
|
||||
"'sha256-pSTVzZsFAqd2U3QYu+BoBDtuJWaPM/+qMy/dBRrhb5Y='; img-src 'self';",
|
||||
"Referrer-Policy": "Same-origin",
|
||||
}
|
||||
|
||||
// Handler returns the Http.Handlers for authentication, callback, and refresh
|
||||
// Handler returns the Http.Handlers for authenticate, callback, and refresh
|
||||
func (p *Authenticate) Handler() http.Handler {
|
||||
// set up our standard middlewares
|
||||
stdMiddleware := middleware.NewChain()
|
||||
|
@ -52,8 +54,6 @@ func (p *Authenticate) Handler() http.Handler {
|
|||
middleware.ValidateSignature(p.SharedKey),
|
||||
middleware.ValidateRedirectURI(p.ProxyRootDomains))
|
||||
|
||||
validateClientSecretMiddleware := stdMiddleware.Append(middleware.ValidateClientSecret(p.SharedKey))
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/robots.txt", stdMiddleware.ThenFunc(p.RobotsTxt))
|
||||
// Identity Provider (IdP) callback endpoints and callbacks
|
||||
|
@ -61,11 +61,7 @@ func (p *Authenticate) Handler() http.Handler {
|
|||
mux.Handle("/oauth2/callback", stdMiddleware.ThenFunc(p.OAuthCallback))
|
||||
// authenticate-server endpoints
|
||||
mux.Handle("/sign_in", validateSignatureMiddleware.ThenFunc(p.SignIn))
|
||||
mux.Handle("/sign_out", validateSignatureMiddleware.ThenFunc(p.SignOut)) // "GET", "POST"
|
||||
mux.Handle("/profile", validateClientSecretMiddleware.ThenFunc(p.GetProfile)) // GET
|
||||
mux.Handle("/validate", validateClientSecretMiddleware.ThenFunc(p.ValidateToken)) // GET
|
||||
mux.Handle("/redeem", validateClientSecretMiddleware.ThenFunc(p.Redeem)) // POST
|
||||
mux.Handle("/refresh", validateClientSecretMiddleware.ThenFunc(p.Refresh)) //POST
|
||||
mux.Handle("/sign_out", validateSignatureMiddleware.ThenFunc(p.SignOut)) // GET POST
|
||||
|
||||
return mux
|
||||
}
|
||||
|
@ -76,43 +72,15 @@ func (p *Authenticate) RobotsTxt(w http.ResponseWriter, r *http.Request) {
|
|||
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
|
||||
}
|
||||
|
||||
// SignInPage directs the user to the sign in page. Takes a `redirect_uri` param.
|
||||
func (p *Authenticate) SignInPage(w http.ResponseWriter, r *http.Request) {
|
||||
redirectURL := p.RedirectURL.ResolveReference(r.URL)
|
||||
|
||||
destinationURL, _ := url.Parse(redirectURL.Query().Get("redirect_uri"))
|
||||
t := struct {
|
||||
ProviderName string
|
||||
AllowedDomains []string
|
||||
Redirect string
|
||||
Destination string
|
||||
Version string
|
||||
}{
|
||||
ProviderName: p.provider.Data().ProviderName,
|
||||
AllowedDomains: p.AllowedDomains,
|
||||
Redirect: redirectURL.String(),
|
||||
Destination: destinationURL.Host,
|
||||
Version: version.FullVersion(),
|
||||
}
|
||||
log.FromRequest(r).Debug().
|
||||
Str("ProviderName", p.provider.Data().ProviderName).
|
||||
Str("Redirect", redirectURL.String()).
|
||||
Str("Destination", destinationURL.Host).
|
||||
Str("AllowedDomains", strings.Join(p.AllowedDomains, ", ")).
|
||||
Msg("authenticate: SignInPage")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
p.templates.ExecuteTemplate(w, "sign_in.html", t)
|
||||
}
|
||||
|
||||
func (p *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*sessions.SessionState, error) {
|
||||
session, err := p.sessionStore.LoadSession(r)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("authenticate: failed to load session")
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed to load session")
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure sessions lifetime has not expired
|
||||
// if long-lived lifetime has expired, clear session
|
||||
if session.LifetimePeriodExpired() {
|
||||
log.FromRequest(r).Warn().Msg("authenticate: lifetime expired")
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
|
@ -120,18 +88,14 @@ func (p *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*se
|
|||
}
|
||||
// check if session refresh period is up
|
||||
if session.RefreshPeriodExpired() {
|
||||
ok, err := p.provider.RefreshSessionIfNeeded(session)
|
||||
newToken, err := p.provider.Refresh(session.RefreshToken)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed to refresh session")
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
log.FromRequest(r).Error().Msg("user unauthorized after refresh")
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
return nil, httputil.ErrUserNotAuthorized
|
||||
}
|
||||
// update refresh'd session in cookie
|
||||
session.AccessToken = newToken.AccessToken
|
||||
session.RefreshDeadline = newToken.Expiry
|
||||
err = p.sessionStore.SaveSession(w, r, session)
|
||||
if err != nil {
|
||||
// We refreshed the session successfully, but failed to save it.
|
||||
|
@ -143,9 +107,9 @@ func (p *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*se
|
|||
}
|
||||
} else {
|
||||
// The session has not exceeded it's lifetime or requires refresh
|
||||
ok := p.provider.ValidateSessionState(session)
|
||||
if !ok {
|
||||
log.FromRequest(r).Error().Msg("invalid session state")
|
||||
ok, err := p.provider.Validate(session.IDToken)
|
||||
if !ok || err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("invalid session state")
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
return nil, httputil.ErrUserNotAuthorized
|
||||
}
|
||||
|
@ -157,68 +121,53 @@ func (p *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*se
|
|||
}
|
||||
}
|
||||
|
||||
// authenticate really should not be in the business of authorization
|
||||
// todo(bdd) : remove when authorization module added
|
||||
if !p.Validator(session.Email) {
|
||||
log.FromRequest(r).Error().Msg("invalid email user")
|
||||
return nil, httputil.ErrUserNotAuthorized
|
||||
}
|
||||
log.Info().Msg("authenticate")
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// SignIn handles the /sign_in endpoint. It attempts to authenticate the user,
|
||||
// and if the user is not authenticated, it renders a sign in page.
|
||||
func (p *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) {
|
||||
// We attempt to authenticate the user. If they cannot be authenticated, we render a sign-in
|
||||
// page.
|
||||
//
|
||||
// If the user is authenticated, we redirect back to the proxy application
|
||||
// at the `redirect_uri`, with a temporary token.
|
||||
//
|
||||
// TODO: It is possible for a user to visit this page without a redirect destination.
|
||||
// Should we allow the user to authenticate? If not, what should be the proposed workflow?
|
||||
|
||||
session, err := p.authenticate(w, r)
|
||||
switch err {
|
||||
case nil:
|
||||
// User is authenticated, redirect back to proxy
|
||||
p.ProxyOAuthRedirect(w, r, session)
|
||||
case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession:
|
||||
log.Debug().Err(err).Msg("authenticate.SignIn")
|
||||
log.Info().Err(err).Msg("authenticate.SignIn : expected failure")
|
||||
if err != http.ErrNoCookie {
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
}
|
||||
p.OAuthStart(w, r)
|
||||
|
||||
default:
|
||||
log.Error().Err(err).Msg("authenticate.SignIn")
|
||||
log.Error().Err(err).Msg("authenticate: unexpected sign in error")
|
||||
httputil.ErrorResponse(w, r, err.Error(), httputil.CodeForError(err))
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyOAuthRedirect redirects the user back to sso proxy's redirection endpoint.
|
||||
// ProxyOAuthRedirect redirects the user back to proxy's redirection endpoint.
|
||||
// This workflow corresponds to Section 3.1.2 of the OAuth2 RFC.
|
||||
// See https://tools.ietf.org/html/rfc6749#section-3.1.2 for more specific information.
|
||||
func (p *Authenticate) ProxyOAuthRedirect(w http.ResponseWriter, r *http.Request, session *sessions.SessionState) {
|
||||
// This workflow corresponds to Section 3.1.2 of the OAuth2 RFC.
|
||||
// See https://tools.ietf.org/html/rfc6749#section-3.1.2 for more specific information.
|
||||
//
|
||||
// We redirect the user back to the proxy application's redirection endpoint; in the
|
||||
// sso proxy, this is the `/oauth/callback` endpoint.
|
||||
//
|
||||
// We must provide the proxy with a temporary authorization code via the `code` parameter,
|
||||
// which they can use to redeem an access token for subsequent API calls.
|
||||
//
|
||||
// We must also include the original `state` parameter received from the proxy application.
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// original `state` parameter received from the proxy application.
|
||||
state := r.Form.Get("state")
|
||||
if state == "" {
|
||||
httputil.ErrorResponse(w, r, "no state parameter supplied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// redirect url of proxy-service
|
||||
redirectURI := r.Form.Get("redirect_uri")
|
||||
if redirectURI == "" {
|
||||
httputil.ErrorResponse(w, r, "no redirect_uri parameter supplied", http.StatusForbidden)
|
||||
|
@ -230,7 +179,7 @@ func (p *Authenticate) ProxyOAuthRedirect(w http.ResponseWriter, r *http.Request
|
|||
httputil.ErrorResponse(w, r, "malformed redirect_uri parameter passed", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// encrypt session state as json blob
|
||||
encrypted, err := sessions.MarshalSession(session, p.cipher)
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
|
@ -267,6 +216,7 @@ func (p *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
|
|||
case nil:
|
||||
break
|
||||
case http.ErrNoCookie: // if there's no cookie in the session we can just redirect
|
||||
log.Error().Err(err).Msg("authenticate.SignOut : no cookie")
|
||||
http.Redirect(w, r, redirectURI, http.StatusFound)
|
||||
return
|
||||
default:
|
||||
|
@ -277,7 +227,7 @@ func (p *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
err = p.provider.Revoke(session)
|
||||
err = p.provider.Revoke(session.AccessToken)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("authenticate.SignOut : error revoking session")
|
||||
p.SignOutPage(w, r, "An error occurred during sign out. Please try again.")
|
||||
|
@ -299,10 +249,11 @@ func (p *Authenticate) SignOutPage(w http.ResponseWriter, r *http.Request, messa
|
|||
|
||||
signature := r.Form.Get("sig")
|
||||
timestamp := r.Form.Get("ts")
|
||||
destinationURL, _ := url.Parse(redirectURI) //checked by middleware
|
||||
destinationURL, err := url.Parse(redirectURI)
|
||||
|
||||
// An error message indicates that an internal server error occurred
|
||||
if message != "" {
|
||||
if message != "" || err != nil {
|
||||
log.Error().Err(err).Msg("authenticate.SignOutPage")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
|
@ -326,8 +277,8 @@ func (p *Authenticate) SignOutPage(w http.ResponseWriter, r *http.Request, messa
|
|||
p.templates.ExecuteTemplate(w, "sign_out.html", t)
|
||||
}
|
||||
|
||||
// OAuthStart starts the authentication process by redirecting to the provider. It provides a
|
||||
// `redirectURI`, allowing the provider to redirect back to the sso proxy after authentication.
|
||||
// OAuthStart starts the authenticate process by redirecting to the provider. It provides a
|
||||
// `redirectURI`, allowing the provider to redirect back to the sso proxy after authenticate.
|
||||
func (p *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
||||
authRedirectURL, err := url.Parse(r.URL.Query().Get("redirect_uri"))
|
||||
if err != nil {
|
||||
|
@ -339,12 +290,12 @@ func (p *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
|||
nonce := fmt.Sprintf("%x", cryptutil.GenerateKey())
|
||||
p.csrfStore.SetCSRF(w, r, nonce)
|
||||
|
||||
// confirm the redirect uri is from the root domain
|
||||
// verify redirect uri is from the root domain
|
||||
if !middleware.ValidRedirectURI(authRedirectURL.String(), p.ProxyRootDomains) {
|
||||
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// confirm proxy url is from the root domain
|
||||
// verify proxy url is from the root domain
|
||||
proxyRedirectURL, err := url.Parse(authRedirectURL.Query().Get("redirect_uri"))
|
||||
if err != nil || !middleware.ValidRedirectURI(proxyRedirectURL.String(), p.ProxyRootDomains) {
|
||||
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
|
||||
|
@ -359,51 +310,41 @@ func (p *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// embed authenticate service's state as the base64'd nonce and authenticate callback url
|
||||
// concat base64'd nonce and authenticate url to make state
|
||||
state := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", nonce, authRedirectURL.String())))
|
||||
// build the provider sign in url
|
||||
signInURL := p.provider.GetSignInURL(state)
|
||||
|
||||
http.Redirect(w, r, signInURL, http.StatusFound)
|
||||
}
|
||||
|
||||
func (p *Authenticate) redeemCode(host, code string) (*sessions.SessionState, error) {
|
||||
session, err := p.provider.Redeem(code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if session.Email == "" {
|
||||
// return nil, fmt.Errorf("no email included in session")
|
||||
// }
|
||||
|
||||
return session, nil
|
||||
|
||||
}
|
||||
|
||||
// getOAuthCallback completes the oauth cycle from an identity provider's callback
|
||||
func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
// finish the oauth cycle
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: bad form on oauth callback")
|
||||
return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: err.Error()}
|
||||
}
|
||||
errorString := r.Form.Get("error")
|
||||
if errorString != "" {
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: provider returned error")
|
||||
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: errorString}
|
||||
}
|
||||
code := r.Form.Get("code")
|
||||
if code == "" {
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: provider missing code")
|
||||
return "", httputil.HTTPError{Code: http.StatusBadRequest, Message: "Missing Code"}
|
||||
}
|
||||
|
||||
session, err := p.redeemCode(r.Host, code)
|
||||
session, err := p.provider.Authenticate(code)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("error redeeming authentication code")
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: error redeeming authenticate code")
|
||||
return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: err.Error()}
|
||||
}
|
||||
|
||||
bytes, err := base64.URLEncoding.DecodeString(r.Form.Get("state"))
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("failed decoding state")
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed decoding state")
|
||||
return "", httputil.HTTPError{Code: http.StatusBadRequest, Message: "Couldn't decode state"}
|
||||
}
|
||||
s := strings.SplitN(string(bytes), ":", 2)
|
||||
|
@ -414,11 +355,12 @@ func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
|
|||
redirect := s[1]
|
||||
c, err := p.csrfStore.GetCSRF(r)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: bad csrf")
|
||||
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Missing CSRF token"}
|
||||
}
|
||||
p.csrfStore.ClearCSRF(w, r)
|
||||
if c.Value != nonce {
|
||||
log.FromRequest(r).Error().Err(err).Msg("CSRF token mismatch")
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: csrf mismatch")
|
||||
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "CSRF failed"}
|
||||
}
|
||||
|
||||
|
@ -427,13 +369,10 @@ func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
|
||||
// Set cookie, or deny: validates the session email and group
|
||||
// - for p.Validator see validator.go#newValidatorImpl for more info
|
||||
// - for p.provider.ValidateGroup see providers/google.go#ValidateGroup for more info
|
||||
if !p.Validator(session.Email) {
|
||||
log.FromRequest(r).Error().Err(err).Str("email", session.Email).Msg("invalid email permissions denied")
|
||||
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "You don't have access"}
|
||||
}
|
||||
log.FromRequest(r).Info().Str("email", session.Email).Msg("authentication complete")
|
||||
err = p.sessionStore.SaveSession(w, r, session)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("internal error")
|
||||
|
@ -442,182 +381,22 @@ func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
|
|||
return redirect, nil
|
||||
}
|
||||
|
||||
// OAuthCallback handles the callback from the provider, and returns an error response if there is an error.
|
||||
// If there is no error it will redirect to the redirect url.
|
||||
// OAuthCallback handles the callback from the identity provider. Displays an error page if there
|
||||
// was an error. If successful, redirects back to the proxy-service via the redirect-url.
|
||||
func (p *Authenticate) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||
redirect, err := p.getOAuthCallback(w, r)
|
||||
switch h := err.(type) {
|
||||
case nil:
|
||||
break
|
||||
case httputil.HTTPError:
|
||||
log.Error().Err(err).Msg("authenticate: oauth callback error")
|
||||
httputil.ErrorResponse(w, r, h.Message, h.Code)
|
||||
return
|
||||
default:
|
||||
log.Error().Err(err).Msg("authenticate.OAuthCallback")
|
||||
log.Error().Err(err).Msg("authenticate: unexpected oauth callback error")
|
||||
httputil.ErrorResponse(w, r, "Internal Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// redirect back to the proxy-service
|
||||
http.Redirect(w, r, redirect, http.StatusFound)
|
||||
}
|
||||
|
||||
// Redeem has a signed access token, and provides the user information associated with the access token.
|
||||
func (p *Authenticate) Redeem(w http.ResponseWriter, r *http.Request) {
|
||||
// The auth code is redeemed by the sso proxy for an access token, refresh token,
|
||||
// expiration, and email.
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Bad Request: %s", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
session, err := sessions.UnmarshalSession(r.Form.Get("code"), p.cipher)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed to unmarshal session")
|
||||
http.Error(w, fmt.Sprintf("invalid auth code: %s", err.Error()), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if session == nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("empty session")
|
||||
http.Error(w, fmt.Sprintf("empty session: %s", err.Error()), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if session != nil && (session.RefreshPeriodExpired() || session.LifetimePeriodExpired()) {
|
||||
log.FromRequest(r).Error().Msg("expired session")
|
||||
p.sessionStore.ClearSession(w, r)
|
||||
http.Error(w, fmt.Sprintf("expired session"), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
response := struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
Email string `json:"email"`
|
||||
}{
|
||||
AccessToken: session.AccessToken,
|
||||
RefreshToken: session.RefreshToken,
|
||||
IDToken: session.IDToken,
|
||||
ExpiresIn: int64(time.Until(session.RefreshDeadline).Seconds()),
|
||||
Email: session.Email,
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("GAP-Auth", session.Email)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(jsonBytes)
|
||||
|
||||
}
|
||||
|
||||
// Refresh takes a refresh token and returns a new access token
|
||||
func (p *Authenticate) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Bad Request: %s", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
refreshToken := r.Form.Get("refresh_token")
|
||||
if refreshToken == "" {
|
||||
http.Error(w, "Bad Request: No Refresh Token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
accessToken, expiresIn, err := p.provider.RefreshAccessToken(refreshToken)
|
||||
if err != nil {
|
||||
httputil.ErrorResponse(w, r, err.Error(), httputil.CodeForError(err))
|
||||
return
|
||||
}
|
||||
|
||||
response := struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}{
|
||||
AccessToken: accessToken,
|
||||
ExpiresIn: int64(expiresIn.Seconds()),
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(bytes)
|
||||
}
|
||||
|
||||
// GetProfile gets a list of groups of which a user is a member.
|
||||
func (p *Authenticate) GetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
// The sso proxy sends the user's email to this endpoint to get a list of Google groups that
|
||||
// the email is a member of. The proxy will compare these groups to the list of allowed
|
||||
// groups for the upstream service the user is trying to access.
|
||||
|
||||
email := r.FormValue("email")
|
||||
if email == "" {
|
||||
http.Error(w, "no email address included", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// groupsFormValue := r.FormValue("groups")
|
||||
// allowedGroups := []string{}
|
||||
// if groupsFormValue != "" {
|
||||
// allowedGroups = strings.Split(groupsFormValue, ",")
|
||||
// }
|
||||
|
||||
// groups, err := p.provider.ValidateGroupMembership(email, allowedGroups)
|
||||
// if err != nil {
|
||||
// log.Error().Err(err).Msg("authenticate.GetProfile : error retrieving groups")
|
||||
// httputil.ErrorResponse(w, r, err.Error(), httputil.CodeForError(err))
|
||||
// return
|
||||
// }
|
||||
|
||||
response := struct {
|
||||
Email string `json:"email"`
|
||||
}{
|
||||
Email: email,
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("error marshaling response: %s", err.Error()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("GAP-Auth", email)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(jsonBytes)
|
||||
}
|
||||
|
||||
// ValidateToken validates the X-Access-Token from the header and returns an error response
|
||||
// if it's invalid
|
||||
func (p *Authenticate) ValidateToken(w http.ResponseWriter, r *http.Request) {
|
||||
accessToken := r.Header.Get("X-Access-Token")
|
||||
idToken := r.Header.Get("X-Id-Token")
|
||||
|
||||
if accessToken == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if idToken == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ok := p.provider.ValidateSessionState(&sessions.SessionState{
|
||||
AccessToken: accessToken,
|
||||
IDToken: idToken,
|
||||
})
|
||||
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
package authenticate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/pomerium/pomerium/authenticate/providers"
|
||||
"github.com/pomerium/pomerium/internal/templates"
|
||||
)
|
||||
|
||||
|
@ -19,7 +17,6 @@ func testAuthenticate() *Authenticate {
|
|||
auth.AllowedDomains = []string{"*"}
|
||||
auth.ProxyRootDomains = []string{"example.com"}
|
||||
auth.templates = templates.New()
|
||||
auth.provider = providers.NewTestProvider(auth.RedirectURL)
|
||||
return &auth
|
||||
}
|
||||
|
||||
|
@ -38,43 +35,5 @@ func TestAuthenticate_RobotsTxt(t *testing.T) {
|
|||
expected := fmt.Sprintf("User-agent: *\nDisallow: /")
|
||||
if rr.Body.String() != expected {
|
||||
t.Errorf("handler returned wrong body: got %v want %v", rr.Body.String(), expected)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate_SignInPage(t *testing.T) {
|
||||
auth := testAuthenticate()
|
||||
v := url.Values{}
|
||||
v.Set("request_uri", "this-is-a-test-uri")
|
||||
url := fmt.Sprintf("/signin?%s", v.Encode())
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(auth.SignInPage)
|
||||
handler.ServeHTTP(rr, req)
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
|
||||
}
|
||||
body := rr.Body.Bytes()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
want bool
|
||||
}{
|
||||
{"provider name", auth.provider.Data().ProviderName, true},
|
||||
{"destination url", v.Encode(), true},
|
||||
{"shouldn't be found", "this string should not be in the body", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := bytes.Contains(body, []byte(tt.value)); got != tt.want {
|
||||
t.Errorf("handler body missing expected value %v", tt.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
5
authenticate/providers/doc.go
Normal file
5
authenticate/providers/doc.go
Normal 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"
|
|
@ -15,7 +15,7 @@ const defaultGitlabProviderURL = "https://gitlab.com"
|
|||
|
||||
// GitlabProvider is an implementation of the Provider interface.
|
||||
type GitlabProvider struct {
|
||||
*ProviderData
|
||||
*IdentityProvider
|
||||
cb *circuit.Breaker
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ type GitlabProvider struct {
|
|||
// - https://docs.gitlab.com/ee/integration/oauth_provider.html
|
||||
// - https://docs.gitlab.com/ee/api/oauth2.html
|
||||
// - https://gitlab.com/.well-known/openid-configuration
|
||||
func NewGitlabProvider(p *ProviderData) (*GitlabProvider, error) {
|
||||
func NewGitlabProvider(p *IdentityProvider) (*GitlabProvider, error) {
|
||||
ctx := context.Background()
|
||||
if p.ProviderURL == "" {
|
||||
p.ProviderURL = defaultGitlabProviderURL
|
||||
|
@ -42,8 +42,9 @@ func NewGitlabProvider(p *ProviderData) (*GitlabProvider, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Scopes = []string{oidc.ScopeOpenID, "read_user"}
|
||||
|
||||
if len(p.Scopes) == 0 {
|
||||
p.Scopes = []string{oidc.ScopeOpenID, "read_user"}
|
||||
}
|
||||
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
|
||||
p.oauth = &oauth2.Config{
|
||||
ClientID: p.ClientID,
|
||||
|
@ -53,7 +54,7 @@ func NewGitlabProvider(p *ProviderData) (*GitlabProvider, error) {
|
|||
Scopes: p.Scopes,
|
||||
}
|
||||
gitlabProvider := &GitlabProvider{
|
||||
ProviderData: p,
|
||||
IdentityProvider: p,
|
||||
}
|
||||
gitlabProvider.cb = circuit.NewBreaker(&circuit.Options{
|
||||
HalfOpenConcurrentRequests: 2,
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
"github.com/pomerium/pomerium/authenticate/circuit"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
|
@ -19,14 +18,14 @@ const defaultGoogleProviderURL = "https://accounts.google.com"
|
|||
|
||||
// GoogleProvider is an implementation of the Provider interface.
|
||||
type GoogleProvider struct {
|
||||
*ProviderData
|
||||
*IdentityProvider
|
||||
cb *circuit.Breaker
|
||||
// non-standard oidc fields
|
||||
RevokeURL *url.URL
|
||||
}
|
||||
|
||||
// NewGoogleProvider returns a new GoogleProvider and sets the provider url endpoints.
|
||||
func NewGoogleProvider(p *ProviderData) (*GoogleProvider, error) {
|
||||
func NewGoogleProvider(p *IdentityProvider) (*GoogleProvider, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
if p.ProviderURL == "" {
|
||||
|
@ -37,18 +36,20 @@ func NewGoogleProvider(p *ProviderData) (*GoogleProvider, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(p.Scopes) == 0 {
|
||||
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email"}
|
||||
}
|
||||
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
|
||||
p.oauth = &oauth2.Config{
|
||||
ClientID: p.ClientID,
|
||||
ClientSecret: p.ClientSecret,
|
||||
Endpoint: p.provider.Endpoint(),
|
||||
RedirectURL: p.RedirectURL.String(),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
Scopes: p.Scopes,
|
||||
}
|
||||
|
||||
googleProvider := &GoogleProvider{
|
||||
ProviderData: p,
|
||||
IdentityProvider: p,
|
||||
}
|
||||
// google supports a revocation endpoint
|
||||
var claims struct {
|
||||
|
@ -91,9 +92,9 @@ func (p *GoogleProvider) cbStateChange(from, to circuit.State) {
|
|||
//
|
||||
// https://developers.google.com/identity/protocols/OAuth2WebServer#tokenrevoke
|
||||
// https://github.com/googleapis/google-api-dotnet-client/issues/1285
|
||||
func (p *GoogleProvider) Revoke(s *sessions.SessionState) error {
|
||||
func (p *GoogleProvider) Revoke(accessToken string) error {
|
||||
params := url.Values{}
|
||||
params.Add("token", s.AccessToken)
|
||||
params.Add("token", accessToken)
|
||||
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil)
|
||||
if err != nil && err != httputil.ErrTokenRevoked {
|
||||
return err
|
||||
|
@ -105,4 +106,5 @@ func (p *GoogleProvider) Revoke(s *sessions.SessionState) error {
|
|||
// Google requires access type offline
|
||||
func (p *GoogleProvider) GetSignInURL(state string) string {
|
||||
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
|
||||
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
"github.com/pomerium/pomerium/authenticate/circuit"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
|
@ -22,7 +21,7 @@ const defaultAzureProviderURL = "https://login.microsoftonline.com/common"
|
|||
|
||||
// AzureProvider is an implementation of the Provider interface
|
||||
type AzureProvider struct {
|
||||
*ProviderData
|
||||
*IdentityProvider
|
||||
cb *circuit.Breaker
|
||||
// non-standard oidc fields
|
||||
RevokeURL *url.URL
|
||||
|
@ -31,7 +30,7 @@ type AzureProvider struct {
|
|||
// NewAzureProvider returns a new AzureProvider and sets the provider url endpoints.
|
||||
// If non-"common" tenant is desired, ProviderURL must be set.
|
||||
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc
|
||||
func NewAzureProvider(p *ProviderData) (*AzureProvider, error) {
|
||||
func NewAzureProvider(p *IdentityProvider) (*AzureProvider, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
if p.ProviderURL == "" {
|
||||
|
@ -43,18 +42,20 @@ func NewAzureProvider(p *ProviderData) (*AzureProvider, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(p.Scopes) == 0 {
|
||||
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "offline_access"}
|
||||
}
|
||||
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
|
||||
p.oauth = &oauth2.Config{
|
||||
ClientID: p.ClientID,
|
||||
ClientSecret: p.ClientSecret,
|
||||
Endpoint: p.provider.Endpoint(),
|
||||
RedirectURL: p.RedirectURL.String(),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
Scopes: p.Scopes,
|
||||
}
|
||||
|
||||
azureProvider := &AzureProvider{
|
||||
ProviderData: p,
|
||||
IdentityProvider: p,
|
||||
}
|
||||
// azure has a "end session endpoint"
|
||||
var claims struct {
|
||||
|
@ -95,9 +96,9 @@ func (p *AzureProvider) cbStateChange(from, to circuit.State) {
|
|||
|
||||
// Revoke revokes the access token a given session state.
|
||||
//https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
|
||||
func (p *AzureProvider) Revoke(s *sessions.SessionState) error {
|
||||
func (p *AzureProvider) Revoke(token string) error {
|
||||
params := url.Values{}
|
||||
params.Add("token", s.AccessToken)
|
||||
params.Add("token", token)
|
||||
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil)
|
||||
if err != nil && err != httputil.ErrTokenRevoked {
|
||||
return err
|
||||
|
|
|
@ -12,11 +12,11 @@ import (
|
|||
// of an authorization identity provider.
|
||||
// see : https://openid.net/specs/openid-connect-core-1_0.html
|
||||
type OIDCProvider struct {
|
||||
*ProviderData
|
||||
*IdentityProvider
|
||||
}
|
||||
|
||||
// NewOIDCProvider creates a new instance of an OpenID Connect provider.
|
||||
func NewOIDCProvider(p *ProviderData) (*OIDCProvider, error) {
|
||||
func NewOIDCProvider(p *IdentityProvider) (*OIDCProvider, error) {
|
||||
ctx := context.Background()
|
||||
if p.ProviderURL == "" {
|
||||
return nil, errors.New("missing required provider url")
|
||||
|
@ -26,13 +26,16 @@ func NewOIDCProvider(p *ProviderData) (*OIDCProvider, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(p.Scopes) == 0 {
|
||||
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "offline_access"}
|
||||
}
|
||||
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
|
||||
p.oauth = &oauth2.Config{
|
||||
ClientID: p.ClientID,
|
||||
ClientSecret: p.ClientSecret,
|
||||
Endpoint: p.provider.Endpoint(),
|
||||
RedirectURL: p.RedirectURL.String(),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
Scopes: p.Scopes,
|
||||
}
|
||||
return &OIDCProvider{ProviderData: p}, nil
|
||||
return &OIDCProvider{IdentityProvider: p}, nil
|
||||
}
|
||||
|
|
|
@ -9,21 +9,20 @@ import (
|
|||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
// OktaProvider provides a standard, OpenID Connect implementation
|
||||
// of an authorization identity provider.
|
||||
type OktaProvider struct {
|
||||
*ProviderData
|
||||
*IdentityProvider
|
||||
|
||||
// non-standard oidc fields
|
||||
RevokeURL *url.URL
|
||||
}
|
||||
|
||||
// NewOktaProvider creates a new instance of an OpenID Connect provider.
|
||||
func NewOktaProvider(p *ProviderData) (*OktaProvider, error) {
|
||||
func NewOktaProvider(p *IdentityProvider) (*OktaProvider, error) {
|
||||
ctx := context.Background()
|
||||
if p.ProviderURL == "" {
|
||||
return nil, errors.New("missing required provider url")
|
||||
|
@ -33,24 +32,26 @@ func NewOktaProvider(p *ProviderData) (*OktaProvider, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(p.Scopes) == 0 {
|
||||
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "offline_access"}
|
||||
}
|
||||
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
|
||||
p.oauth = &oauth2.Config{
|
||||
ClientID: p.ClientID,
|
||||
ClientSecret: p.ClientSecret,
|
||||
Endpoint: p.provider.Endpoint(),
|
||||
RedirectURL: p.RedirectURL.String(),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
Scopes: p.Scopes,
|
||||
}
|
||||
oktaProvider := OktaProvider{ProviderData: p}
|
||||
|
||||
// okta supports a revocation endpoint
|
||||
var claims struct {
|
||||
RevokeURL string `json:"revocation_endpoint"`
|
||||
}
|
||||
|
||||
if err := p.provider.Claims(&claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oktaProvider := OktaProvider{IdentityProvider: p}
|
||||
|
||||
oktaProvider.RevokeURL, err = url.Parse(claims.RevokeURL)
|
||||
if err != nil {
|
||||
|
@ -61,11 +62,11 @@ func NewOktaProvider(p *ProviderData) (*OktaProvider, error) {
|
|||
|
||||
// Revoke revokes the access token a given session state.
|
||||
// https://developer.okta.com/docs/api/resources/oidc#revoke
|
||||
func (p *OktaProvider) Revoke(s *sessions.SessionState) error {
|
||||
func (p *OktaProvider) Revoke(token string) error {
|
||||
params := url.Values{}
|
||||
params.Add("client_id", p.ClientID)
|
||||
params.Add("client_secret", p.ClientSecret)
|
||||
params.Add("token", s.IDToken)
|
||||
params.Add("token", token)
|
||||
params.Add("token_type_hint", "refresh_token")
|
||||
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil)
|
||||
if err != nil && err != httputil.ErrTokenRevoked {
|
||||
|
@ -73,3 +74,9 @@ func (p *OktaProvider) Revoke(s *sessions.SessionState) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSignInURL returns the sign in url with typical oauth parameters
|
||||
// Google requires access type offline
|
||||
func (p *OktaProvider) GetSignInURL(state string) string {
|
||||
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
//go:generate protoc -I ../../proto/authenticate --go_out=plugins=grpc:../../proto/authenticate ../../proto/authenticate/authenticate.proto
|
||||
|
||||
package providers // import "github.com/pomerium/pomerium/internal/providers"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
|
@ -32,21 +31,17 @@ const (
|
|||
|
||||
// Provider is an interface exposing functions necessary to interact with a given provider.
|
||||
type Provider interface {
|
||||
Data() *ProviderData
|
||||
Redeem(string) (*sessions.SessionState, error)
|
||||
ValidateSessionState(*sessions.SessionState) bool
|
||||
Authenticate(string) (*sessions.SessionState, error)
|
||||
Validate(string) (bool, error)
|
||||
Refresh(string) (*oauth2.Token, error)
|
||||
Revoke(string) error
|
||||
GetSignInURL(state string) string
|
||||
RefreshSessionIfNeeded(*sessions.SessionState) (bool, error)
|
||||
Revoke(*sessions.SessionState) error
|
||||
RefreshAccessToken(string) (string, time.Duration, error)
|
||||
}
|
||||
|
||||
// New returns a new identity provider based given its name.
|
||||
// Returns an error if selected provided not found or if the provider fails to instantiate.
|
||||
func New(provider string, pd *ProviderData) (Provider, error) {
|
||||
var err error
|
||||
var p Provider
|
||||
switch provider {
|
||||
// Returns an error if selected provided not found or if the identity provider is not known.
|
||||
func New(providerName string, pd *IdentityProvider) (p Provider, err error) {
|
||||
switch providerName {
|
||||
case AzureProviderName:
|
||||
p, err = NewAzureProvider(pd)
|
||||
case GitlabProviderName:
|
||||
|
@ -58,7 +53,7 @@ func New(provider string, pd *ProviderData) (Provider, error) {
|
|||
case OktaProviderName:
|
||||
p, err = NewOktaProvider(pd)
|
||||
default:
|
||||
return nil, fmt.Errorf("authenticate: provider %q not found", provider)
|
||||
return nil, fmt.Errorf("authenticate: %q name not found", providerName)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -66,11 +61,13 @@ func New(provider string, pd *ProviderData) (Provider, error) {
|
|||
return p, nil
|
||||
}
|
||||
|
||||
// ProviderData holds the fields associated with providers
|
||||
// necessary to implement the Provider interface.
|
||||
type ProviderData struct {
|
||||
// IdentityProvider contains the fields required for an OAuth 2.0 Authorization Request that
|
||||
// requests that the End-User be authenticated by the Authorization Server.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
type IdentityProvider struct {
|
||||
ProviderName string
|
||||
|
||||
RedirectURL *url.URL
|
||||
ProviderName string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ProviderURL string
|
||||
|
@ -82,118 +79,50 @@ type ProviderData struct {
|
|||
oauth *oauth2.Config
|
||||
}
|
||||
|
||||
// Data returns a ProviderData.
|
||||
func (p *ProviderData) Data() *ProviderData { return p }
|
||||
|
||||
// GetSignInURL returns the sign in url with typical oauth parameters
|
||||
func (p *ProviderData) GetSignInURL(state string) string {
|
||||
// GetSignInURL returns a URL to OAuth 2.0 provider's consent page
|
||||
// that asks for permissions for the required scopes explicitly.
|
||||
//
|
||||
// State is a token to protect the user from CSRF attacks. You must
|
||||
// always provide a non-empty string and validate that it matches the
|
||||
// the state query parameter on your redirect callback.
|
||||
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
||||
func (p *IdentityProvider) GetSignInURL(state string) string {
|
||||
return p.oauth.AuthCodeURL(state)
|
||||
}
|
||||
|
||||
// ValidateSessionState validates a given session's from it's JWT token
|
||||
// Validate validates a given session's from it's JWT token
|
||||
// The function verifies it's been signed by the provider, preforms
|
||||
// any additional checks depending on the Config, and returns the payload.
|
||||
//
|
||||
// ValidateSessionState does NOT do nonce validation.
|
||||
func (p *ProviderData) ValidateSessionState(s *sessions.SessionState) bool {
|
||||
// Validate does NOT do nonce validation.
|
||||
// Validate does NOT check if revoked.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||
func (p *IdentityProvider) Validate(idToken string) (bool, error) {
|
||||
ctx := context.Background()
|
||||
_, err := p.verifier.Verify(ctx, s.IDToken)
|
||||
_, err := p.verifier.Verify(ctx, idToken)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("authenticate/providers: failed to verify session state")
|
||||
return false
|
||||
return false, err
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Redeem creates a session with an identity provider from a authorization code
|
||||
func (p *ProviderData) Redeem(code string) (*sessions.SessionState, error) {
|
||||
ctx := context.Background()
|
||||
// convert authorization code into a token
|
||||
token, err := p.oauth.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authenticate/providers: failed token exchange: %v", err)
|
||||
}
|
||||
s, err := p.createSessionState(ctx, token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authenticate/providers: unable to update session: %v", err)
|
||||
}
|
||||
|
||||
// check if provider has info endpoint, try to hit that and gather more info
|
||||
// especially useful if initial request did not contain email
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||
var claims struct {
|
||||
UserInfoURL string `json:"userinfo_endpoint"`
|
||||
}
|
||||
|
||||
if err := p.provider.Claims(&claims); err != nil || claims.UserInfoURL == "" {
|
||||
log.Error().Err(err).Msg("authenticate/providers: failed retrieving userinfo_endpoint")
|
||||
} else {
|
||||
// userinfo endpoint found and valid
|
||||
userInfo, err := p.UserInfo(ctx, claims.UserInfoURL, oauth2.StaticTokenSource(token))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authenticate/providers: can't parse userinfo_endpoint: %v", err)
|
||||
}
|
||||
s.Email = userInfo.Email
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// RefreshSessionIfNeeded will refresh the session state if it's deadline is expired
|
||||
func (p *ProviderData) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
|
||||
if !sessionRefreshRequired(s) {
|
||||
log.Debug().Msg("authenticate/providers: session refresh not needed")
|
||||
return false, nil
|
||||
}
|
||||
origExpiration := s.RefreshDeadline
|
||||
err := p.redeemRefreshToken(s)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("authenticate/providers: couldn't refresh token: %v", err)
|
||||
}
|
||||
|
||||
log.Debug().Time("NewDeadline", s.RefreshDeadline).Time("OldDeadline", origExpiration).Msgf("authenticate/providers refreshed")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (p *ProviderData) redeemRefreshToken(s *sessions.SessionState) error {
|
||||
log.Info().Msg("authenticate/providers.oidc.redeemRefreshToken 1")
|
||||
// Authenticate creates a session with an identity provider from a authorization code
|
||||
func (p *IdentityProvider) Authenticate(code string) (*sessions.SessionState, error) {
|
||||
ctx := context.Background()
|
||||
t := &oauth2.Token{
|
||||
RefreshToken: s.RefreshToken,
|
||||
Expiry: time.Now().Add(-time.Hour),
|
||||
}
|
||||
log.Info().Msg("authenticate/providers.oidc.redeemRefreshToken 3")
|
||||
|
||||
// returns a TokenSource automatically refreshing it as necessary using the provided context
|
||||
token, err := p.oauth.TokenSource(ctx, t).Token()
|
||||
// convert authorization code into a token
|
||||
oauth2Token, err := p.oauth.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return fmt.Errorf("authenticate/providers: failed to get token: %v", err)
|
||||
return nil, fmt.Errorf("authenticate/providers: failed token exchange: %v", err)
|
||||
}
|
||||
log.Info().Msg("authenticate/providers.oidc.redeemRefreshToken 4")
|
||||
|
||||
newSession, err := p.createSessionState(ctx, token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("authenticate/providers: unable to update session: %v", err)
|
||||
}
|
||||
s.AccessToken = newSession.AccessToken
|
||||
s.IDToken = newSession.IDToken
|
||||
s.RefreshToken = newSession.RefreshToken
|
||||
s.RefreshDeadline = newSession.RefreshDeadline
|
||||
s.Email = newSession.Email
|
||||
|
||||
log.Info().
|
||||
Str("AccessToken", s.AccessToken).
|
||||
Str("IdToken", s.IDToken).
|
||||
Time("RefreshDeadline", s.RefreshDeadline).
|
||||
Str("RefreshToken", s.RefreshToken).
|
||||
Str("Email", s.Email).
|
||||
Msg("authenticate/providers.redeemRefreshToken")
|
||||
Str("RefreshToken", oauth2Token.RefreshToken).
|
||||
Str("TokenType", oauth2Token.TokenType).
|
||||
Str("AccessToken", oauth2Token.AccessToken).
|
||||
Msg("Authenticate - oauth.Exchange")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProviderData) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
//id_token contains claims about the authenticated user
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("token response did not contain an id_token")
|
||||
}
|
||||
|
@ -204,142 +133,51 @@ func (p *ProviderData) createSessionState(ctx context.Context, token *oauth2.Tok
|
|||
return nil, fmt.Errorf("authenticate/providers: could not verify id_token: %v", err)
|
||||
}
|
||||
|
||||
// Extract custom claims.
|
||||
// Extract id_token which contains claims about the authenticated user
|
||||
var claims struct {
|
||||
Email string `json:"email"`
|
||||
Verified *bool `json:"email_verified"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
// parse claims from the raw, encoded jwt token
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return nil, fmt.Errorf("authenticate/providers: failed to parse id_token claims: %v", err)
|
||||
}
|
||||
log.Debug().
|
||||
Str("AccessToken", token.AccessToken).
|
||||
Str("IDToken", rawIDToken).
|
||||
Str("claims.Email", claims.Email).
|
||||
Str("RefreshToken", token.RefreshToken).
|
||||
Str("idToken.Subject", idToken.Subject).
|
||||
Str("idToken.Nonce", idToken.Nonce).
|
||||
Str("RefreshDeadline", idToken.Expiry.String()).
|
||||
Str("LifetimeDeadline", idToken.Expiry.String()).
|
||||
Msg("authenticate/providers.createSessionState")
|
||||
|
||||
return &sessions.SessionState{
|
||||
AccessToken: token.AccessToken,
|
||||
IDToken: rawIDToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
RefreshDeadline: idToken.Expiry,
|
||||
LifetimeDeadline: idToken.Expiry,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry,
|
||||
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: claims.Groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshAccessToken allows the service to refresh an access token without
|
||||
// prompting the user for permission.
|
||||
func (p *ProviderData) RefreshAccessToken(refreshToken string) (string, time.Duration, error) {
|
||||
// Refresh renews a user's session using an access token without reprompting the user.
|
||||
func (p *IdentityProvider) Refresh(refreshToken string) (*oauth2.Token, error) {
|
||||
if refreshToken == "" {
|
||||
return "", 0, errors.New("authenticate/providers: missing refresh token")
|
||||
}
|
||||
ctx := context.Background()
|
||||
c := oauth2.Config{
|
||||
ClientID: p.ClientID,
|
||||
ClientSecret: p.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{TokenURL: p.ProviderURL},
|
||||
return nil, errors.New("authenticate/providers: missing refresh token")
|
||||
}
|
||||
t := oauth2.Token{RefreshToken: refreshToken}
|
||||
ts := c.TokenSource(ctx, &t)
|
||||
newToken, err := p.oauth.TokenSource(context.Background(), &t).Token()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("authenticate/providers.Refresh")
|
||||
return nil, err
|
||||
}
|
||||
log.Info().
|
||||
Str("RefreshToken", refreshToken).
|
||||
Msg("authenticate/providers.RefreshAccessToken")
|
||||
Str("newToken.AccessToken", newToken.AccessToken).
|
||||
Str("time.Until(newToken.Expiry)", time.Until(newToken.Expiry).String()).
|
||||
Msg("authenticate/providers.Refresh")
|
||||
|
||||
newToken, err := ts.Token()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("authenticate/providers.RefreshAccessToken")
|
||||
return "", 0, err
|
||||
}
|
||||
return newToken.AccessToken, time.Until(newToken.Expiry), nil
|
||||
return newToken, nil
|
||||
}
|
||||
|
||||
// Revoke enables a user to revoke her token. If the identity provider supports revocation
|
||||
// the endpoint is available, otherwise an error is thrown.
|
||||
func (p *ProviderData) Revoke(s *sessions.SessionState) error {
|
||||
func (p *IdentityProvider) Revoke(token string) error {
|
||||
return errors.New("authenticate/providers: revoke not implemented")
|
||||
}
|
||||
|
||||
func sessionRefreshRequired(s *sessions.SessionState) bool {
|
||||
return s == nil || s.RefreshDeadline.After(time.Now()) || s.RefreshToken == ""
|
||||
}
|
||||
|
||||
// UserInfo represents the OpenID Connect userinfo claims.
|
||||
// see: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||
type UserInfo struct {
|
||||
// Stanard OIDC User fields
|
||||
Subject string `json:"sub"`
|
||||
Profile string `json:"profile"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
// custom claims
|
||||
Name string `json:"name"` // google, gitlab
|
||||
GivenName string `json:"given_name"` // google
|
||||
FamilyName string `json:"family_name"` // google
|
||||
Picture string `json:"picture"` // google,gitlab
|
||||
Locale string `json:"locale"` // google
|
||||
Groups []string `json:"groups"` // gitlab
|
||||
|
||||
claims []byte
|
||||
}
|
||||
|
||||
// Claims unmarshals the raw JSON object claims into the provided object.
|
||||
func (u *UserInfo) Claims(v interface{}) error {
|
||||
if u.claims == nil {
|
||||
return errors.New("authenticate/providers: claims not set")
|
||||
}
|
||||
return json.Unmarshal(u.claims, v)
|
||||
}
|
||||
|
||||
// UserInfo uses the token source to query the provider's user info endpoint.
|
||||
func (p *ProviderData) UserInfo(ctx context.Context, uri string, tokenSource oauth2.TokenSource) (*UserInfo, error) {
|
||||
if uri == "" {
|
||||
return nil, errors.New("authenticate/providers: user info endpoint is not supported by this provider")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, uri, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authenticate/providers: create GET request: %v", err)
|
||||
}
|
||||
|
||||
token, err := tokenSource.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authenticate/providers: get access token: %v", err)
|
||||
}
|
||||
token.SetAuthHeader(req)
|
||||
|
||||
resp, err := doRequest(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s: %s", resp.Status, body)
|
||||
}
|
||||
|
||||
var userInfo UserInfo
|
||||
if err := json.Unmarshal(body, &userInfo); err != nil {
|
||||
return nil, fmt.Errorf("authenticate/providers failed to decode userinfo: %v", err)
|
||||
}
|
||||
userInfo.claims = body
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
||||
client := http.DefaultClient
|
||||
if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
|
||||
client = c
|
||||
}
|
||||
return client.Do(req.WithContext(ctx))
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
// }
|
|
@ -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
|
||||
|
||||
}
|
|
@ -6,11 +6,15 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/pomerium/pomerium/authenticate"
|
||||
"github.com/pomerium/pomerium/internal/https"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/middleware"
|
||||
"github.com/pomerium/pomerium/internal/options"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
pb "github.com/pomerium/pomerium/proto/authenticate"
|
||||
"github.com/pomerium/pomerium/proxy"
|
||||
)
|
||||
|
||||
|
@ -34,6 +38,10 @@ func main() {
|
|||
}
|
||||
log.Info().Str("version", version.FullVersion()).Msg("cmd/pomerium")
|
||||
|
||||
grpcAuth := middleware.NewSharedSecretCred(mainOpts.SharedKey)
|
||||
grpcOpts := []grpc.ServerOption{grpc.UnaryInterceptor(grpcAuth.ValidateRequest)}
|
||||
grpcServer := grpc.NewServer(grpcOpts...)
|
||||
|
||||
var authenticateService *authenticate.Authenticate
|
||||
var authHost string
|
||||
if mainOpts.Services == "all" || mainOpts.Services == "authenticate" {
|
||||
|
@ -51,6 +59,8 @@ func main() {
|
|||
log.Fatal().Err(err).Msg("cmd/pomerium: new authenticate")
|
||||
}
|
||||
authHost = authOpts.RedirectURL.Host
|
||||
pb.RegisterAuthenticatorServer(grpcServer, authenticateService)
|
||||
|
||||
}
|
||||
|
||||
var proxyService *proxy.Proxy
|
||||
|
@ -64,6 +74,7 @@ func main() {
|
|||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cmd/pomerium: new proxy")
|
||||
}
|
||||
defer proxyService.AuthenticateConn.Close()
|
||||
}
|
||||
|
||||
topMux := http.NewServeMux()
|
||||
|
@ -85,5 +96,7 @@ func main() {
|
|||
CertFile: mainOpts.CertFile,
|
||||
KeyFile: mainOpts.KeyFile,
|
||||
}
|
||||
log.Fatal().Err(https.ListenAndServeTLS(httpOpts, topMux)).Msg("cmd/pomerium: https serve failure")
|
||||
|
||||
log.Fatal().Err(https.ListenAndServeTLS(httpOpts, topMux, grpcServer)).Msg("cmd/pomerium: https serve failure")
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main // import "github.com/pomerium/pomerium/cmd/pomerium"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/pomerium/envconfig"
|
||||
|
@ -14,6 +15,11 @@ type Options struct {
|
|||
// Debug enables more verbose logging, and outputs human-readable logs to Stdout.
|
||||
// Set with POMERIUM_DEBUG
|
||||
Debug bool `envconfig:"POMERIUM_DEBUG"`
|
||||
|
||||
// SharedKey is the shared secret authorization key used to mutually authenticate
|
||||
// requests between services.
|
||||
SharedKey string `envconfig:"SHARED_SECRET"`
|
||||
|
||||
// Services is a list enabled service mode. If none are selected, "all" is used.
|
||||
// Available options are : "all", "authenticate", "proxy".
|
||||
Services string `envconfig:"SERVICES"`
|
||||
|
@ -33,7 +39,7 @@ var defaultOptions = &Options{
|
|||
Services: "all",
|
||||
}
|
||||
|
||||
// optionsFromEnvConfig builds the authentication service's configuration
|
||||
// optionsFromEnvConfig builds the IdentityProvider service's configuration
|
||||
// options from provided environmental variables
|
||||
func optionsFromEnvConfig() (*Options, error) {
|
||||
o := defaultOptions
|
||||
|
@ -43,6 +49,9 @@ func optionsFromEnvConfig() (*Options, error) {
|
|||
if !isValidService(o.Services) {
|
||||
return nil, fmt.Errorf("%s is an invalid service type", o.Services)
|
||||
}
|
||||
if o.SharedKey == "" {
|
||||
return nil, errors.New("shared-key cannot be empty")
|
||||
}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,9 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
os.Clearenv()
|
||||
}
|
||||
func Test_optionsFromEnvConfig(t *testing.T) {
|
||||
good := defaultOptions
|
||||
good.SharedKey = "test"
|
||||
tests := []struct {
|
||||
name string
|
||||
want *Options
|
||||
|
@ -17,18 +16,25 @@ func Test_optionsFromEnvConfig(t *testing.T) {
|
|||
envValue string
|
||||
wantErr bool
|
||||
}{
|
||||
{"good default with no env settings", defaultOptions, "", "", false},
|
||||
{"good service", defaultOptions, "SERVICES", "all", false},
|
||||
{"good default with no env settings", good, "", "", false},
|
||||
{"invalid service type", nil, "SERVICES", "invalid", true},
|
||||
{"good service", good, "SERVICES", "all", false},
|
||||
|
||||
{"bad debug boolean", nil, "POMERIUM_DEBUG", "yes", true},
|
||||
{"missing shared secret", nil, "SHARED_SECRET", "", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
os.Clearenv()
|
||||
if tt.envKey != "" {
|
||||
os.Setenv(tt.envKey, tt.envValue)
|
||||
defer os.Unsetenv(tt.envKey)
|
||||
}
|
||||
if tt.envKey != "SHARED_SECRET" {
|
||||
os.Setenv("SHARED_SECRET", "test")
|
||||
}
|
||||
got, err := optionsFromEnvConfig()
|
||||
os.Unsetenv(tt.envKey)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("optionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
|
|
@ -34,18 +34,29 @@ Uses the [latest pomerium build](https://hub.docker.com/r/pomerium/pomerium) fro
|
|||
|
||||
- Minimal container-based configuration.
|
||||
- Docker and Docker-Compose based.
|
||||
- Uses pre-configured built-in nginx load balancer
|
||||
- Runs separate containers for each service
|
||||
- Comes with a pre-configured instance of on-prem Gitlab-CE
|
||||
- Runs a single container for all pomerium services
|
||||
- Routes default to on-prem [helloworld], [httpbin] containers.
|
||||
|
||||
Customize for your identity provider run `docker-compose up -f basic.docker-compose.yml`
|
||||
|
||||
#### basic.docker-compose.yml
|
||||
|
||||
<<< @/docs/docs/examples/basic.docker-compose.yml
|
||||
<<< @/docs/docs/examples/docker/basic.docker-compose.yml
|
||||
|
||||
### Gitlab On-premise
|
||||
### NGINX micro-services
|
||||
|
||||
- Docker and Docker-Compose based.
|
||||
- Uses pre-configured built-in nginx load balancer
|
||||
- Runs separate containers for each service
|
||||
- Routes default to on-prem [helloworld], [httpbin], and [gitlab] containers.
|
||||
|
||||
Customize for your identity provider run `docker-compose up -f gitlab.docker-compose.yml`
|
||||
|
||||
#### nginx.docker-compose.yml
|
||||
|
||||
<<< @/docs/docs/examples/docker/nginx.docker-compose.yml
|
||||
|
||||
### Gitlab On-Prem
|
||||
|
||||
- Docker and Docker-Compose based.
|
||||
- Uses pre-configured built-in nginx load balancer
|
||||
|
@ -57,7 +68,7 @@ Customize for your identity provider run `docker-compose up -f gitlab.docker-com
|
|||
|
||||
#### gitlab.docker-compose.yml
|
||||
|
||||
<<< @/docs/docs/examples/gitlab.docker-compose.yml
|
||||
<<< @/docs/docs/examples/docker/gitlab.docker-compose.yml
|
||||
|
||||
## Kubernetes
|
||||
|
||||
|
|
|
@ -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
|
54
docs/docs/examples/docker/basic.docker-compose.yml
Normal file
54
docs/docs/examples/docker/basic.docker-compose.yml
Normal 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
|
|
@ -2,7 +2,7 @@ version: "3"
|
|||
|
||||
services:
|
||||
nginx:
|
||||
image: jwilder/nginx-proxy:latest
|
||||
image: pomerium/nginx-proxy:latest
|
||||
ports:
|
||||
- "443:443"
|
||||
volumes:
|
||||
|
@ -17,18 +17,17 @@ services:
|
|||
pomerium-authenticate:
|
||||
build: .
|
||||
restart: always
|
||||
depends_on:
|
||||
- "gitlab"
|
||||
environment:
|
||||
- POMERIUM_DEBUG=true
|
||||
- SERVICES=authenticate
|
||||
# auth settings
|
||||
- REDIRECT_URL=https://sso-auth.corp.beyondperimeter.com/oauth2/callback
|
||||
- IDP_PROVIDER="gitlab"
|
||||
- IDP_PROVIDER_URL=https://gitlab.corp.beyondperimeter.com
|
||||
- IDP_CLIENT_ID=022dbbd09402441dc7af1924b679bc5e6f5bf0d7a555e55b38c51e2e4e6cee76
|
||||
- IDP_CLIENT_SECRET=fb7598c520c346915ee369eee57688938fe4f31329a308c4669074da562714b2
|
||||
- PROXY_ROOT_DOMAIN=beyondperimeter.com
|
||||
- REDIRECT_URL=https://auth.corp.beyondperimeter.com/oauth2/callback
|
||||
# Identity Provider Settings (Must be changed!)
|
||||
- IDP_PROVIDER=google
|
||||
- IDP_PROVIDER_URL=https://accounts.google.com
|
||||
- IDP_CLIENT_ID=REPLACEME
|
||||
- IDP_CLIENT_SECRET=REPLACE_ME
|
||||
- PROXY_ROOT_DOMAIN=corp.beyondperimeter.com
|
||||
- ALLOWED_DOMAINS=*
|
||||
- SKIP_PROVIDER_BUTTON=false
|
||||
# shared service settings
|
||||
|
@ -36,14 +35,13 @@ services:
|
|||
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
|
||||
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
|
||||
- VIRTUAL_PROTO=https
|
||||
- VIRTUAL_HOST=sso-auth.corp.beyondperimeter.com
|
||||
- VIRTUAL_HOST=auth.corp.beyondperimeter.com
|
||||
- VIRTUAL_PORT=443
|
||||
volumes: # volumes is optional; used if passing certificates as files
|
||||
- ./cert.pem:/pomerium/cert.pem:ro
|
||||
- ./privkey.pem:/pomerium/privkey.pem:ro
|
||||
expose:
|
||||
- 443
|
||||
|
||||
pomerium-proxy:
|
||||
build: .
|
||||
restart: always
|
||||
|
@ -51,12 +49,17 @@ services:
|
|||
- POMERIUM_DEBUG=true
|
||||
- SERVICES=proxy
|
||||
# proxy settings
|
||||
- AUTHENTICATE_SERVICE_URL=https://sso-auth.corp.beyondperimeter.com
|
||||
- ROUTES=https://httpbin.corp.beyondperimeter.com=http://httpbin,https://hello.corp.beyondperimeter.com=http://hello-world/
|
||||
- AUTHENTICATE_SERVICE_URL=https://auth.corp.beyondperimeter.com
|
||||
# IMPORTANT! If you are running pomerium behind another ingress (loadbalancer/firewall/etc)
|
||||
# you must tell pomerium proxy how to communicate using an internal hostname for RPC
|
||||
- AUTHENTICATE_INTERNAL_URL=pomerium-authenticate:443
|
||||
# When communicating internally, rPC is going to get a name conflict expecting an external
|
||||
# facing certificate name (i.e. authenticate-service.local vs *.corp.example.com).
|
||||
- OVERIDE_CERTIFICATE_NAME=*.corp.beyondperimeter.com
|
||||
- ROUTES=https://gitlab.corp.beyondperimeter.com=https://gitlab
|
||||
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
|
||||
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
|
||||
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
|
||||
- SIGNING_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
# nginx settings
|
||||
- VIRTUAL_PROTO=https
|
||||
- VIRTUAL_HOST=*.corp.beyondperimeter.com
|
83
docs/docs/examples/docker/nginx.docker-compose.yml
Normal file
83
docs/docs/examples/docker/nginx.docker-compose.yml
Normal 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
|
|
@ -16,7 +16,7 @@ spec:
|
|||
app: pomerium-authenticate
|
||||
spec:
|
||||
containers:
|
||||
- image: pomerium/pomerium:latest
|
||||
- image: pomerium/pomerium:grpctest
|
||||
name: pomerium-authenticate
|
||||
ports:
|
||||
- containerPort: 443
|
||||
|
@ -26,7 +26,7 @@ spec:
|
|||
- name: SERVICES
|
||||
value: authenticate
|
||||
- name: REDIRECT_URL
|
||||
value: https://sso-auth.corp.beyondperimeter.com/oauth2/callback
|
||||
value: https://auth.corp.beyondperimeter.com/oauth2/callback
|
||||
- name: IDP_PROVIDER
|
||||
value: google
|
||||
- name: IDP_PROVIDER_URL
|
||||
|
@ -62,12 +62,6 @@ spec:
|
|||
secretKeyRef:
|
||||
name: certificate-key
|
||||
key: certificate-key
|
||||
- name: VIRTUAL_PROTO
|
||||
value: https
|
||||
- name: VIRTUAL_HOST
|
||||
value: sso-auth.corp.beyondperimeter.com
|
||||
- name: VIRTUAL_PORT
|
||||
value: "443"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
|
|
34
docs/docs/examples/kubernetes/ingress.nginx.yml
Normal file
34
docs/docs/examples/kubernetes/ingress.nginx.yml
Normal 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
|
|
@ -12,28 +12,20 @@ spec:
|
|||
- secretName: pomerium-tls
|
||||
hosts:
|
||||
- "*.corp.beyondperimeter.com"
|
||||
- "sso-auth.corp.beyondperimeter.com"
|
||||
- "auth.corp.beyondperimeter.com"
|
||||
rules:
|
||||
- host: "*.corp.beyondperimeter.com"
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
- paths:
|
||||
backend:
|
||||
serviceName: pomerium-proxy-service
|
||||
servicePort: 443
|
||||
- path: /*
|
||||
backend:
|
||||
serviceName: pomerium-proxy-service
|
||||
servicePort: 443
|
||||
servicePort: https
|
||||
|
||||
- host: "sso-auth.corp.beyondperimeter.com"
|
||||
- host: "auth.corp.beyondperimeter.com"
|
||||
http:
|
||||
paths:
|
||||
- path: /*
|
||||
- paths:
|
||||
backend:
|
||||
serviceName: pomerium-authenticate-service
|
||||
servicePort: 443
|
||||
- path: /
|
||||
backend:
|
||||
serviceName: pomerium-authenticate-service
|
||||
servicePort: 443
|
||||
servicePort: https
|
||||
|
|
|
@ -16,7 +16,7 @@ spec:
|
|||
app: pomerium-proxy
|
||||
spec:
|
||||
containers:
|
||||
- image: pomerium/pomerium:latest
|
||||
- image: pomerium/pomerium:grpctest
|
||||
name: pomerium-proxy
|
||||
ports:
|
||||
- containerPort: 443
|
||||
|
@ -24,11 +24,15 @@ spec:
|
|||
protocol: TCP
|
||||
env:
|
||||
- name: ROUTES
|
||||
value: https://httpbin.corp.beyondperimeter.com=https://httpbin.org
|
||||
value: https://httpbin.corp.beyondperimeter.com=https://httpbin.org,https://hi.corp.beyondperimeter.com=http://hello-app.pomerium.svc.cluster.local:8080
|
||||
- name: SERVICES
|
||||
value: proxy
|
||||
- name: AUTHENTICATE_SERVICE_URL
|
||||
value: https://sso-auth.corp.beyondperimeter.com
|
||||
value: https://auth.corp.beyondperimeter.com
|
||||
- name: AUTHENTICATE_INTERNAL_URL
|
||||
value: "pomerium-authenticate-service.pomerium.svc.cluster.local:443"
|
||||
- name: OVERIDE_CERTIFICATE_NAME
|
||||
value: "*.corp.beyondperimeter.com"
|
||||
- name: SHARED_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
@ -54,12 +58,6 @@ spec:
|
|||
secretKeyRef:
|
||||
name: certificate-key
|
||||
key: certificate-key
|
||||
- name: VIRTUAL_PROTO
|
||||
value: https
|
||||
- name: VIRTUAL_HOST
|
||||
value: "*.corp.beyondperimeter.com"
|
||||
- name: VIRTUAL_PORT
|
||||
value: "443"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
|
|
|
@ -10,9 +10,9 @@ description: >-
|
|||
|
||||
This article describes how to configure pomerium to use a third-party identity service for single-sign-on.
|
||||
|
||||
There are a few configuration steps required for identity provider integration. Most providers support [OpenID Connect] which provides a standardized interface for authentication. In this guide we'll cover how to do the following for each identity provider:
|
||||
There are a few configuration steps required for identity provider integration. Most providers support [OpenID Connect] which provides a standardized interface for IdentityProvider. In this guide we'll cover how to do the following for each identity provider:
|
||||
|
||||
1. Establish a **Redirect URL** with the identity provider which is called after authentication.
|
||||
1. Establish a **Redirect URL** with the identity provider which is called after IdentityProvider.
|
||||
2. Generate a **Client ID** and **Client Secret**.
|
||||
3. Configure pomerium to use the **Client ID** and **Client Secret** keys.
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ Run [./scripts/kubernetes_gke.sh] which will:
|
|||
1. Provision a new cluster
|
||||
2. Create authenticate and proxy [deployments](https://cloud.google.com/kubernetes-engine/docs/concepts/deployment).
|
||||
3. Provision and apply authenticate and proxy [services](https://cloud.google.com/kubernetes-engine/docs/concepts/service).
|
||||
4. Configure an ingress to do serve TLS between client and load balancer
|
||||
4. Configure an ingress load balancer.
|
||||
|
||||
```bash
|
||||
sh ./scripts/kubernetes_gke.sh
|
||||
|
|
|
@ -20,7 +20,7 @@ Place your domain's wild-card TLS certificate next to the compose file. If you d
|
|||
|
||||
## Run
|
||||
|
||||
Docker-compose will automatically download the latest pomerium release as well as two example containers and an nginx load balancer all in one step.
|
||||
Docker-compose will automatically download the latest pomerium release as well as two example containers.
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
|
|
5
go.mod
5
go.mod
|
@ -3,6 +3,8 @@ module github.com/pomerium/pomerium
|
|||
require (
|
||||
github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/golang/mock v1.2.0
|
||||
github.com/golang/protobuf v1.2.0
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31
|
||||
github.com/pomerium/go-oidc v2.0.0+incompatible
|
||||
|
@ -10,10 +12,11 @@ require (
|
|||
github.com/rs/zerolog v1.11.0
|
||||
github.com/stretchr/testify v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
|
||||
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 // indirect
|
||||
google.golang.org/appengine v1.4.0 // indirect
|
||||
google.golang.org/grpc v1.18.0
|
||||
gopkg.in/square/go-jose.v2 v2.2.1
|
||||
)
|
||||
|
|
22
go.sum
22
go.sum
|
@ -1,9 +1,18 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3 h1:wOysYcIdqv3WnvwqFFzrYCFALPED7qkUGaLXu359GSc=
|
||||
github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3/go.mod h1:UMqtWQTnOe4byzwe7Zhwh8f8s+36uszN51sJrSIZlTE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31 h1:bNqUesLWa+RUxQvSaV3//dEFviXdCSvMF9GKDOopFLU=
|
||||
|
@ -18,17 +27,30 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
|
|||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0=
|
||||
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA=
|
||||
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
gopkg.in/square/go-jose.v2 v2.2.1 h1:uRIz/V7RfMsMgGnCp+YybIdstDIz8wc0H283wHQfwic=
|
||||
gopkg.in/square/go-jose.v2 v2.2.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
|
@ -3,12 +3,12 @@ version: 0.0.1
|
|||
apiVersion: v1
|
||||
appVersion: 0.0.1
|
||||
home: http://www.pomerium.io/
|
||||
description: A reverse proxy that provides authentication with Google, Okta, Azure or other providers
|
||||
description: A reverse proxy that provides IdentityProvider with Google, Okta, Azure or other providers
|
||||
keywords:
|
||||
- kubernetes
|
||||
- oauth
|
||||
- oauth2
|
||||
- authentication
|
||||
- IdentityProvider
|
||||
- google
|
||||
- okta
|
||||
- azure
|
||||
|
|
|
@ -24,7 +24,7 @@ proxy:
|
|||
|
||||
# For any other settings that are optional
|
||||
# ADDRESS, POMERIUM_DEBUG, CERTIFICATE_FILE, CERTIFICATE_KEY_FILE
|
||||
# PROXY_ROOT_DOMAIN, COOKIE_DOMAIN, COOKIE_EXPIRE, COOKIE_REFRESH, COOKIE_SECURE, COOKIE_HTTP_ONLY, IDP_SCOPE
|
||||
# PROXY_ROOT_DOMAIN, COOKIE_DOMAIN, COOKIE_EXPIRE, COOKIE_REFRESH, COOKIE_SECURE, COOKIE_HTTP_ONLY, IDP_SCOPES
|
||||
# DEFAULT_UPSTREAM_TIMEOUT, PASS_ACCESS_TOKEN, SESSION_VALID_TTL, SESSION_LIFETIME_TTL, GRACE_PERIOD_TTL
|
||||
extraEnv: {}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ type XChaCha20Cipher struct {
|
|||
aead cipher.AEAD
|
||||
}
|
||||
|
||||
// NewCipher returns a new XChacha20poly1305 cipher.
|
||||
// NewCipher takes secret key and returns a new XChacha20poly1305 cipher.
|
||||
func NewCipher(secret []byte) (*XChaCha20Cipher, error) {
|
||||
aead, err := chacha20poly1305.NewX(secret)
|
||||
if err != nil {
|
||||
|
|
|
@ -8,9 +8,11 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/fileutil"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Options contains the configurations settings for a TLS http server.
|
||||
|
@ -55,7 +57,7 @@ func (opt *Options) applyDefaults() {
|
|||
|
||||
// ListenAndServeTLS serves the provided handlers by HTTPS
|
||||
// using the provided options.
|
||||
func ListenAndServeTLS(opt *Options, handler http.Handler) error {
|
||||
func ListenAndServeTLS(opt *Options, httpHandler http.Handler, grpcHandler *grpc.Server) error {
|
||||
if opt == nil {
|
||||
opt = defaultOptions
|
||||
} else {
|
||||
|
@ -82,16 +84,21 @@ func ListenAndServeTLS(opt *Options, handler http.Handler) error {
|
|||
|
||||
ln = tls.NewListener(ln, config)
|
||||
|
||||
var h http.Handler
|
||||
if grpcHandler == nil {
|
||||
h = httpHandler
|
||||
} else {
|
||||
h = grpcHandlerFunc(grpcHandler, httpHandler)
|
||||
}
|
||||
// Set up the main server.
|
||||
server := &http.Server{
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
// WriteTimeout is set to 0 because it also pertains to
|
||||
// streaming replies, e.g., the DirServer.Watch interface.
|
||||
ReadTimeout: 10 * time.Second,
|
||||
// WriteTimeout is set to 0 for streaming replies
|
||||
WriteTimeout: 0,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
TLSConfig: config,
|
||||
Handler: handler,
|
||||
Handler: h,
|
||||
}
|
||||
|
||||
return server.Serve(ln)
|
||||
|
@ -130,10 +137,15 @@ func readCertificateFile(certFile, certKeyFile string) (*tls.Certificate, error)
|
|||
}
|
||||
|
||||
// newDefaultTLSConfig creates a new TLS config based on the certificate files given.
|
||||
// see also:
|
||||
// See :
|
||||
// https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_configurations
|
||||
// https://blog.cloudflare.com/exposing-go-on-the-internet/
|
||||
// https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices
|
||||
// https://github.com/golang/go/blob/df91b8044dbe790c69c16058330f545be069cc1f/src/crypto/tls/common.go#L919
|
||||
func newDefaultTLSConfig(cert *tls.Certificate) (*tls.Config, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
// Prioritize cipher suites sped up by AES-NI (AES-GCM)
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
|
@ -142,10 +154,29 @@ func newDefaultTLSConfig(cert *tls.Certificate) (*tls.Config, error) {
|
|||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
PreferServerCipherSuites: true,
|
||||
Certificates: []tls.Certificate{*cert},
|
||||
// Use curves which have assembly implementations
|
||||
CurvePreferences: []tls.CurveID{
|
||||
tls.CurveP256,
|
||||
tls.X25519,
|
||||
},
|
||||
Certificates: []tls.Certificate{*cert},
|
||||
// HTTP/2 must be enabled manually when using http.Serve
|
||||
NextProtos: []string{"h2"},
|
||||
}
|
||||
tlsConfig.BuildNameToCertificate()
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
// grpcHandlerFunc splits request serving between gRPC and HTTPS depending on the request type.
|
||||
// Requires HTTP/2.
|
||||
func grpcHandlerFunc(rpcServer *grpc.Server, other http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ct := r.Header.Get("Content-Type")
|
||||
if r.ProtoMajor == 2 && strings.Contains(ct, "application/grpc") {
|
||||
rpcServer.ServeHTTP(w, r)
|
||||
} else {
|
||||
other.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
)
|
||||
|
||||
// ErrTokenRevoked signifies a token revokation or expiration error
|
||||
|
@ -59,6 +61,7 @@ func Client(method, endpoint, userAgent string, params url.Values, response inte
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info().Msgf("%s", respBody)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
switch resp.StatusCode {
|
||||
|
|
|
@ -10,11 +10,12 @@ import (
|
|||
)
|
||||
|
||||
// Logger is the global logger.
|
||||
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
var Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()
|
||||
|
||||
// SetDebugMode tells the logger to use standard out and pretty print output.
|
||||
func SetDebugMode() {
|
||||
Logger = Logger.Output(zerolog.ConsoleWriter{Out: os.Stdout})
|
||||
// zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
}
|
||||
|
||||
// With creates a child logger with the field added to its context.
|
||||
|
|
48
internal/middleware/grpc.go
Normal file
48
internal/middleware/grpc.go
Normal 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)
|
||||
}
|
|
@ -119,7 +119,7 @@ func ValidateHost(mux map[string]http.Handler) func(next http.Handler) http.Hand
|
|||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := mux[r.Host]; !ok {
|
||||
httputil.ErrorResponse(w, r, "Unknown host to route", http.StatusNotFound)
|
||||
httputil.ErrorResponse(w, r, "Unknown route", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
|
|
|
@ -16,15 +16,16 @@ var (
|
|||
type SessionState struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IDToken string `json:"id_token"` // https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
|
||||
IDToken string `json:"id_token"`
|
||||
|
||||
RefreshDeadline time.Time `json:"refresh_deadline"`
|
||||
LifetimeDeadline time.Time `json:"lifetime_deadline"`
|
||||
ValidDeadline time.Time `json:"valid_deadline"`
|
||||
GracePeriodStart time.Time `json:"grace_period_start"`
|
||||
|
||||
Email string `json:"email"`
|
||||
User string `json:"user"`
|
||||
Email string `json:"email"`
|
||||
User string `json:"user"` // 'sub' in jwt parlance
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
|
||||
// LifetimePeriodExpired returns true if the lifetime has expired
|
||||
|
|
|
@ -2,11 +2,12 @@ package templates // import "github.com/pomerium/pomerium/internal/templates"
|
|||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/testutil"
|
||||
)
|
||||
|
||||
func TestTemplatesCompile(t *testing.T) {
|
||||
templates := New()
|
||||
testutil.NotEqual(t, templates, nil)
|
||||
if templates == nil {
|
||||
t.Errorf("unexpected nil value %#v", templates)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
477
proto/authenticate/authenticate.pb.go
Normal file
477
proto/authenticate/authenticate.pb.go
Normal 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,
|
||||
}
|
32
proto/authenticate/authenticate.proto
Normal file
32
proto/authenticate/authenticate.proto
Normal 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;
|
||||
}
|
142
proto/authenticate/mock_authenticate/authenticate_mock_test.go
Normal file
142
proto/authenticate/mock_authenticate/authenticate_mock_test.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
97
proto/authenticate/mock_authenticate/mock_authenticate.go
Normal file
97
proto/authenticate/mock_authenticate/mock_authenticate.go
Normal 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...)
|
||||
}
|
|
@ -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
80
proxy/grpc.go
Normal 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
204
proxy/grpc_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
package proxy // import "github.com/pomerium/pomerium/proxy"
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -17,7 +20,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
//ErrUserNotAuthorized is set when user is not authorized to access a resource
|
||||
// ErrUserNotAuthorized is set when user is not authorized to access a resource
|
||||
ErrUserNotAuthorized = errors.New("user not authorized")
|
||||
)
|
||||
|
||||
|
@ -27,6 +30,12 @@ var securityHeaders = map[string]string{
|
|||
"X-XSS-Protection": "1; mode=block",
|
||||
}
|
||||
|
||||
// StateParameter holds the redirect id along with the session id.
|
||||
type StateParameter struct {
|
||||
SessionID string `json:"session_id"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
// Handler returns a http handler for an Proxy
|
||||
func (p *Proxy) Handler() http.Handler {
|
||||
// routes
|
||||
|
@ -61,6 +70,8 @@ func (p *Proxy) Handler() http.Handler {
|
|||
c = c.Append(middleware.RefererHandler("referer"))
|
||||
c = c.Append(middleware.RequestIDHandler("req_id", "Request-Id"))
|
||||
c = c.Append(middleware.ValidateHost(p.mux))
|
||||
|
||||
// serve the middleware and mux
|
||||
h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.ServeHTTP(w, r)
|
||||
}))
|
||||
|
@ -74,7 +85,7 @@ func (p *Proxy) RobotsTxt(w http.ResponseWriter, _ *http.Request) {
|
|||
}
|
||||
|
||||
// Favicon will proxy the request as usual if the user is already authenticated but responds
|
||||
// with a 404 otherwise, to avoid spurious and confusing authentication attempts when a browser
|
||||
// with a 404 otherwise, to avoid spurious and confusing IdentityProvider attempts when a browser
|
||||
// automatically requests the favicon on an error page.
|
||||
func (p *Proxy) Favicon(w http.ResponseWriter, r *http.Request) {
|
||||
err := p.Authenticate(w, r)
|
||||
|
@ -93,15 +104,13 @@ func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
|
|||
Host: r.Host,
|
||||
Path: "/",
|
||||
}
|
||||
fullURL := p.authenticateClient.GetSignOutURL(redirectURL)
|
||||
fullURL := p.GetSignOutURL(p.AuthenticateURL, redirectURL)
|
||||
http.Redirect(w, r, fullURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
// OAuthStart begins the authentication flow, encrypting the redirect url
|
||||
// OAuthStart begins the IdentityProvider flow, encrypting the redirect url
|
||||
// in a request to the provider's sign in endpoint.
|
||||
func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
||||
// The proxy redirects to the authenticator, and provides it with redirectURI (which points
|
||||
// back to the sso proxy).
|
||||
requestURI := r.URL.String()
|
||||
callbackURL := p.GetRedirectURL(r.Host)
|
||||
|
||||
|
@ -129,19 +138,20 @@ func (p *Proxy) OAuthStart(w http.ResponseWriter, r *http.Request) {
|
|||
httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
signinURL := p.authenticateClient.GetSignInURL(callbackURL, encryptedState)
|
||||
log.FromRequest(r).Info().Msg("redirecting to begin auth flow")
|
||||
signinURL := p.GetSignInURL(p.AuthenticateURL, callbackURL, encryptedState)
|
||||
log.FromRequest(r).Info().
|
||||
Str("SigninURL", signinURL.String()).
|
||||
Msg("redirecting to begin auth flow")
|
||||
// redirect the user to the IdentityProvider provider along with the encrypted state which
|
||||
// contains a redirect uri pointing back to the proxy
|
||||
http.Redirect(w, r, signinURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
// OAuthCallback validates the cookie sent back from the provider, then validates he user
|
||||
// information, and if authorized, redirects the user back to the original application.
|
||||
// OAuthCallback validates the cookie sent back from the authenticate service. This function will
|
||||
// contain an error, or it will contain a `code`; the code can be used to fetch an access token, and
|
||||
// other metadata, from the authenticator.
|
||||
// finish the oauth cycle
|
||||
func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||
// We receive the callback from the SSO Authenticator. This request will either contain an
|
||||
// error, or it will contain a `code`; the code can be used to fetch an access token, and
|
||||
// other metadata, from the authenticator.
|
||||
// finish the oauth cycle
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("failed parsing request form")
|
||||
|
@ -153,9 +163,8 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
|||
httputil.ErrorResponse(w, r, errorString, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// We begin the process of redeeming the code for an access token.
|
||||
session, err := p.redeemCode(r.Host, r.Form.Get("code"))
|
||||
session, err := p.AuthenticateRedeem(r.Form.Get("code"))
|
||||
if err != nil {
|
||||
log.FromRequest(r).Error().Err(err).Msg("error redeeming authorization code")
|
||||
httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError)
|
||||
|
@ -208,6 +217,14 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
log.FromRequest(r).Info().
|
||||
Str("code", r.Form.Get("code")).
|
||||
Str("state", r.Form.Get("state")).
|
||||
Str("RefreshToken", session.RefreshToken).
|
||||
Str("session", session.AccessToken).
|
||||
Str("RedirectURI", stateParameter.RedirectURI).
|
||||
Msg("session")
|
||||
|
||||
// This is the redirect back to the original requested application
|
||||
http.Redirect(w, r, stateParameter.RedirectURI, http.StatusFound)
|
||||
}
|
||||
|
@ -222,16 +239,14 @@ func (p *Proxy) AuthenticateOnly(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// Proxy authenticates a request, either proxying the request if it is authenticated,
|
||||
// or starting the authentication process if not.
|
||||
// or starting the IdentityProvider service for validation if not.
|
||||
func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
|
||||
// Attempts to validate the user and their cookie.
|
||||
err := p.Authenticate(w, r)
|
||||
// If the authentication is not successful we proceed to start the OAuth Flow with
|
||||
// If the IdentityProvider is not successful we proceed to start the OAuth Flow with
|
||||
// OAuthStart. If successful, we proceed to proxy to the configured upstream.
|
||||
if err != nil {
|
||||
switch err {
|
||||
case ErrUserNotAuthorized:
|
||||
//todo(bdd) : custom forbidden page with details and troubleshooting info
|
||||
log.FromRequest(r).Debug().Err(err).Msg("proxy: user access forbidden")
|
||||
httputil.ErrorResponse(w, r, "You don't have access", http.StatusForbidden)
|
||||
return
|
||||
|
@ -245,6 +260,9 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
}
|
||||
// ! ! !
|
||||
// todo(bdd): ! Authorization checks will go here !
|
||||
// ! ! !
|
||||
|
||||
// We have validated the users request and now proxy their request to the provided upstream.
|
||||
route, ok := p.router(r)
|
||||
|
@ -270,29 +288,38 @@ func (p *Proxy) Authenticate(w http.ResponseWriter, r *http.Request) (err error)
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if session.LifetimePeriodExpired() {
|
||||
log.FromRequest(r).Info().Msg("proxy.Authenticate: lifetime expired, restarting")
|
||||
return sessions.ErrLifetimeExpired
|
||||
} else if session.RefreshPeriodExpired() {
|
||||
ok, err := p.authenticateClient.RefreshSession(session)
|
||||
}
|
||||
if session.RefreshPeriodExpired() {
|
||||
// AccessToken's usually expire after 60 or so minutes. If offline_access scope is set, a
|
||||
// refresh token (which doesn't change) can be used to request a new access-token. If access
|
||||
// is revoked by identity provider, or no refresh token is set request will return an error
|
||||
accessToken, expiry, err := p.AuthenticateRefresh(session.RefreshToken)
|
||||
if err != nil {
|
||||
log.FromRequest(r).Warn().
|
||||
Str("RefreshToken", session.RefreshToken).
|
||||
Str("AccessToken", session.AccessToken).
|
||||
Msg("proxy.Authenticate: refresh failure")
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return ErrUserNotAuthorized
|
||||
}
|
||||
} else if session.ValidationPeriodExpired() {
|
||||
ok := p.authenticateClient.ValidateSessionState(session)
|
||||
if !ok {
|
||||
return ErrUserNotAuthorized
|
||||
}
|
||||
session.AccessToken = accessToken
|
||||
session.RefreshDeadline = expiry
|
||||
log.FromRequest(r).Info().
|
||||
Str("RefreshToken", session.RefreshToken).
|
||||
Str("AccessToken", session.AccessToken).
|
||||
Msg("proxy.Authenticate: refresh success")
|
||||
}
|
||||
|
||||
err = p.sessionStore.SaveSession(w, r, session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// pass user & user-email details to client applications
|
||||
r.Header.Set(HeaderUserID, session.User)
|
||||
r.Header.Set(HeaderEmail, session.Email)
|
||||
|
||||
// This user has been OK'd. Allow the request!
|
||||
return nil
|
||||
}
|
||||
|
@ -316,7 +343,6 @@ func (p *Proxy) router(r *http.Request) (http.Handler, bool) {
|
|||
// GetRedirectURL returns the redirect url for a given Proxy,
|
||||
// setting the scheme to be https if CookieSecure is true.
|
||||
func (p *Proxy) GetRedirectURL(host string) *url.URL {
|
||||
// TODO: Ensure that we only allow valid upstream hosts in redirect URIs
|
||||
u := p.redirectURL
|
||||
// Build redirect URI from request host
|
||||
if u.Scheme == "" {
|
||||
|
@ -326,19 +352,39 @@ func (p *Proxy) GetRedirectURL(host string) *url.URL {
|
|||
return u
|
||||
}
|
||||
|
||||
func (p *Proxy) redeemCode(host, code string) (*sessions.SessionState, error) {
|
||||
if code == "" {
|
||||
return nil, errors.New("missing code")
|
||||
}
|
||||
redirectURL := p.GetRedirectURL(host)
|
||||
s, err := p.authenticateClient.Redeem(redirectURL.String(), code)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
if s.Email == "" {
|
||||
return s, errors.New("invalid email address")
|
||||
}
|
||||
|
||||
return s, nil
|
||||
// signRedirectURL signs the redirect url string, given a timestamp, and returns it
|
||||
func (p *Proxy) signRedirectURL(rawRedirect string, timestamp time.Time) string {
|
||||
h := hmac.New(sha256.New, []byte(p.SharedKey))
|
||||
h.Write([]byte(rawRedirect))
|
||||
h.Write([]byte(fmt.Sprint(timestamp.Unix())))
|
||||
return base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// GetSignInURL with typical oauth parameters
|
||||
func (p *Proxy) GetSignInURL(authenticateURL, redirectURL *url.URL, state string) *url.URL {
|
||||
a := authenticateURL.ResolveReference(&url.URL{Path: "/sign_in"})
|
||||
now := time.Now()
|
||||
rawRedirect := redirectURL.String()
|
||||
params, _ := url.ParseQuery(a.RawQuery)
|
||||
params.Set("redirect_uri", rawRedirect)
|
||||
params.Set("shared_secret", p.SharedKey)
|
||||
params.Set("response_type", "code")
|
||||
params.Add("state", state)
|
||||
params.Set("ts", fmt.Sprint(now.Unix()))
|
||||
params.Set("sig", p.signRedirectURL(rawRedirect, now))
|
||||
a.RawQuery = params.Encode()
|
||||
return a
|
||||
}
|
||||
|
||||
// GetSignOutURL creates and returns the sign out URL, given a redirectURL
|
||||
func (p *Proxy) GetSignOutURL(authenticateURL, redirectURL *url.URL) *url.URL {
|
||||
a := authenticateURL.ResolveReference(&url.URL{Path: "/sign_out"})
|
||||
now := time.Now()
|
||||
rawRedirect := redirectURL.String()
|
||||
params, _ := url.ParseQuery(a.RawQuery)
|
||||
params.Add("redirect_uri", rawRedirect)
|
||||
params.Set("ts", fmt.Sprint(now.Unix()))
|
||||
params.Set("sig", p.signRedirectURL(rawRedirect, now))
|
||||
a.RawQuery = params.Encode()
|
||||
return a
|
||||
}
|
||||
|
|
158
proxy/proxy.go
158
proxy/proxy.go
|
@ -1,6 +1,8 @@
|
|||
package proxy // import "github.com/pomerium/pomerium/proxy"
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -13,11 +15,15 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pomerium/envconfig"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/middleware"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/templates"
|
||||
"github.com/pomerium/pomerium/proxy/authenticator"
|
||||
pb "github.com/pomerium/pomerium/proto/authenticate"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -29,10 +35,13 @@ const (
|
|||
HeaderEmail = "x-pomerium-authenticated-user-email"
|
||||
)
|
||||
|
||||
// Options represents the configuration options for the proxy service.
|
||||
// Options represents the configurations available for the proxy service.
|
||||
type Options struct {
|
||||
// AuthenticateServiceURL specifies the url to the pomerium authenticate http service.
|
||||
AuthenticateServiceURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
|
||||
// Authenticate service settings
|
||||
AuthenticateURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
|
||||
AuthenticateInternalURL string `envconfig:"AUTHENTICATE_INTERNAL_URL"`
|
||||
//
|
||||
OverideCertificateName string `envconfig:"OVERIDE_CERTIFICATE_NAME"`
|
||||
|
||||
// SigningKey is a base64 encoded private key used to add a JWT-signature to proxied requests.
|
||||
// See : https://www.pomerium.io/guide/signed-headers.html
|
||||
|
@ -40,37 +49,33 @@ type Options struct {
|
|||
// SharedKey is a 32 byte random key used to authenticate access between services.
|
||||
SharedKey string `envconfig:"SHARED_SECRET"`
|
||||
|
||||
DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"`
|
||||
// Session/Cookie management
|
||||
CookieName string
|
||||
CookieSecret string `envconfig:"COOKIE_SECRET"`
|
||||
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
|
||||
CookieSecure bool `envconfig:"COOKIE_SECURE"`
|
||||
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
|
||||
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
|
||||
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
|
||||
CookieLifetimeTTL time.Duration `envconfig:"COOKIE_LIFETIME"`
|
||||
|
||||
CookieName string `envconfig:"COOKIE_NAME"`
|
||||
CookieSecret string `envconfig:"COOKIE_SECRET"`
|
||||
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
|
||||
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
|
||||
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
|
||||
|
||||
PassAccessToken bool `envconfig:"PASS_ACCESS_TOKEN"`
|
||||
|
||||
// session details
|
||||
SessionValidTTL time.Duration `envconfig:"SESSION_VALID_TTL"`
|
||||
SessionLifetimeTTL time.Duration `envconfig:"SESSION_LIFETIME_TTL"`
|
||||
GracePeriodTTL time.Duration `envconfig:"GRACE_PERIOD_TTL"`
|
||||
|
||||
Routes map[string]string `envconfig:"ROUTES"`
|
||||
// Sub-routes
|
||||
Routes map[string]string `envconfig:"ROUTES"`
|
||||
DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"`
|
||||
}
|
||||
|
||||
// NewOptions returns a new options struct
|
||||
var defaultOptions = &Options{
|
||||
CookieName: "_pomerium_proxy",
|
||||
CookieHTTPOnly: false,
|
||||
CookieHTTPOnly: true,
|
||||
CookieSecure: true,
|
||||
CookieExpire: time.Duration(168) * time.Hour,
|
||||
CookieRefresh: time.Duration(30) * time.Minute,
|
||||
CookieLifetimeTTL: time.Duration(720) * time.Hour,
|
||||
DefaultUpstreamTimeout: time.Duration(10) * time.Second,
|
||||
SessionLifetimeTTL: time.Duration(720) * time.Hour,
|
||||
SessionValidTTL: time.Duration(1) * time.Minute,
|
||||
GracePeriodTTL: time.Duration(3) * time.Hour,
|
||||
PassAccessToken: false,
|
||||
}
|
||||
|
||||
// OptionsFromEnvConfig builds the authentication service's configuration
|
||||
// OptionsFromEnvConfig builds the IdentityProvider service's configuration
|
||||
// options from provided environmental variables
|
||||
func OptionsFromEnvConfig() (*Options, error) {
|
||||
o := defaultOptions
|
||||
|
@ -94,11 +99,11 @@ func (o *Options) Validate() error {
|
|||
return fmt.Errorf("could not parse destination %s as url : %q", to, err)
|
||||
}
|
||||
}
|
||||
if o.AuthenticateServiceURL == nil {
|
||||
return errors.New("missing setting: provider-url")
|
||||
if o.AuthenticateURL == nil {
|
||||
return errors.New("missing setting: authenticate-service-url")
|
||||
}
|
||||
if o.AuthenticateServiceURL.Scheme != "https" {
|
||||
return errors.New("provider-url must be a valid https url")
|
||||
if o.AuthenticateURL.Scheme != "https" {
|
||||
return errors.New("authenticate-service-url must be a valid https url")
|
||||
}
|
||||
if o.CookieSecret == "" {
|
||||
return errors.New("missing setting: cookie-secret")
|
||||
|
@ -124,26 +129,29 @@ func (o *Options) Validate() error {
|
|||
|
||||
// Proxy stores all the information associated with proxying a request.
|
||||
type Proxy struct {
|
||||
PassAccessToken bool
|
||||
SharedKey string
|
||||
|
||||
// services
|
||||
authenticateClient *authenticator.AuthenticateClient
|
||||
// Authenticate Service Configuration
|
||||
AuthenticateURL *url.URL
|
||||
AuthenticateInternalURL string
|
||||
AuthenticatorClient pb.AuthenticatorClient
|
||||
// AuthenticateConn must be closed by Proxy's caller
|
||||
AuthenticateConn *grpc.ClientConn
|
||||
|
||||
OverideCertificateName string
|
||||
// session
|
||||
cipher cryptutil.Cipher
|
||||
csrfStore sessions.CSRFStore
|
||||
sessionStore sessions.SessionStore
|
||||
cipher cryptutil.Cipher
|
||||
csrfStore sessions.CSRFStore
|
||||
sessionStore sessions.SessionStore
|
||||
CookieExpire time.Duration
|
||||
CookieRefresh time.Duration
|
||||
CookieLifetimeTTL time.Duration
|
||||
|
||||
redirectURL *url.URL
|
||||
templates *template.Template
|
||||
mux map[string]http.Handler
|
||||
}
|
||||
|
||||
// StateParameter holds the redirect id along with the session id.
|
||||
type StateParameter struct {
|
||||
SessionID string `json:"session_id"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
// New takes a Proxy service from options and a validation function.
|
||||
// Function returns an error if options fail to validate.
|
||||
func New(opts *Options) (*Proxy, error) {
|
||||
|
@ -173,27 +181,21 @@ func New(opts *Options) (*Proxy, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
authClient := authenticator.NewClient(
|
||||
opts.AuthenticateServiceURL,
|
||||
opts.SharedKey,
|
||||
// todo(bdd): fields below should be passed as function args
|
||||
opts.SessionValidTTL,
|
||||
opts.SessionLifetimeTTL,
|
||||
opts.GracePeriodTTL,
|
||||
)
|
||||
|
||||
p := &Proxy{
|
||||
// these fields make up the routing mechanism
|
||||
mux: make(map[string]http.Handler),
|
||||
// session state
|
||||
cipher: cipher,
|
||||
csrfStore: cookieStore,
|
||||
sessionStore: cookieStore,
|
||||
|
||||
authenticateClient: authClient,
|
||||
redirectURL: &url.URL{Path: "/.pomerium/callback"},
|
||||
templates: templates.New(),
|
||||
PassAccessToken: opts.PassAccessToken,
|
||||
cipher: cipher,
|
||||
csrfStore: cookieStore,
|
||||
sessionStore: cookieStore,
|
||||
AuthenticateURL: opts.AuthenticateURL,
|
||||
AuthenticateInternalURL: opts.AuthenticateInternalURL,
|
||||
OverideCertificateName: opts.OverideCertificateName,
|
||||
SharedKey: opts.SharedKey,
|
||||
redirectURL: &url.URL{Path: "/.pomerium/callback"},
|
||||
templates: templates.New(),
|
||||
CookieExpire: opts.CookieExpire,
|
||||
CookieLifetimeTTL: opts.CookieLifetimeTTL,
|
||||
}
|
||||
|
||||
for from, to := range opts.Routes {
|
||||
|
@ -205,8 +207,42 @@ func New(opts *Options) (*Proxy, error) {
|
|||
return nil, err
|
||||
}
|
||||
p.Handle(fromURL.Host, handler)
|
||||
log.Info().Str("from", fromURL.Host).Str("to", toURL.String()).Msg("proxy.New : route")
|
||||
log.Info().Str("from", fromURL.Host).Str("to", toURL.String()).Msg("proxy.New: new route")
|
||||
}
|
||||
// if no port given, assume https/443
|
||||
port := p.AuthenticateURL.Port()
|
||||
if port == "" {
|
||||
port = "443"
|
||||
}
|
||||
authEndpoint := fmt.Sprintf("%s:%s", p.AuthenticateURL.Host, port)
|
||||
|
||||
cp, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.AuthenticateInternalURL != "" {
|
||||
authEndpoint = p.AuthenticateInternalURL
|
||||
}
|
||||
|
||||
log.Info().Str("authEndpoint", authEndpoint).Msgf("proxy.New: grpc authenticate connection")
|
||||
cert := credentials.NewTLS(&tls.Config{RootCAs: cp})
|
||||
if p.OverideCertificateName != "" {
|
||||
err = cert.OverrideServerName(p.OverideCertificateName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
grpcAuth := middleware.NewSharedSecretCred(p.SharedKey)
|
||||
p.AuthenticateConn, err = grpc.Dial(
|
||||
authEndpoint,
|
||||
grpc.WithTransportCredentials(cert),
|
||||
grpc.WithPerRPCCredentials(grpcAuth),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.AuthenticatorClient = pb.NewAuthenticatorClient(p.AuthenticateConn)
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
@ -260,8 +296,8 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
u.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// NewReverseProxy creates a reverse proxy to a specified url.
|
||||
// It adds an X-Forwarded-Host header that is the request's host.
|
||||
// NewReverseProxy returns a new ReverseProxy that routes URLs to the scheme, host, and
|
||||
// base path provided in target. NewReverseProxy rewrites the Host header.
|
||||
func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
|
||||
proxy := httputil.NewSingleHostReverseProxy(to)
|
||||
proxy.Transport = defaultUpstreamTransport
|
||||
|
|
|
@ -11,10 +11,9 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
os.Clearenv()
|
||||
}
|
||||
func TestOptionsFromEnvConfig(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
want *Options
|
||||
|
@ -23,9 +22,9 @@ func TestOptionsFromEnvConfig(t *testing.T) {
|
|||
wantErr bool
|
||||
}{
|
||||
{"good default, no env settings", defaultOptions, "", "", false},
|
||||
{"bad url", nil, "AUTHENTICATE_SERVICE_URL", "%.rjlw", true},
|
||||
{"good duration", defaultOptions, "SESSION_VALID_TTL", "1m", false},
|
||||
{"bad duration", nil, "SESSION_VALID_TTL", "1sm", true},
|
||||
{"bad url", nil, "AUTHENTICATE_SERVICE_URL", "%.ugly", true},
|
||||
{"good duration", defaultOptions, "COOKIE_REFRESH", "1m", false},
|
||||
{"bad duration", nil, "COOKIE_REFRESH", "1sm", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -46,6 +45,8 @@ func TestOptionsFromEnvConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_urlParse(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
uri string
|
||||
|
@ -131,10 +132,10 @@ func TestNewReverseProxyHandler(t *testing.T) {
|
|||
func testOptions() *Options {
|
||||
authurl, _ := url.Parse("https://sso-auth.corp.beyondperimeter.com")
|
||||
return &Options{
|
||||
Routes: map[string]string{"corp.example.com": "example.com"},
|
||||
AuthenticateServiceURL: authurl,
|
||||
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
|
||||
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
|
||||
Routes: map[string]string{"corp.example.com": "example.com"},
|
||||
AuthenticateURL: authurl,
|
||||
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
|
||||
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,10 +146,10 @@ func TestOptions_Validate(t *testing.T) {
|
|||
badToRoute := testOptions()
|
||||
badToRoute.Routes = map[string]string{"^": "example.com"}
|
||||
badAuthURL := testOptions()
|
||||
badAuthURL.AuthenticateServiceURL = nil
|
||||
badAuthURL.AuthenticateURL = nil
|
||||
authurl, _ := url.Parse("http://sso-auth.corp.beyondperimeter.com")
|
||||
httpAuthURL := testOptions()
|
||||
httpAuthURL.AuthenticateServiceURL = authurl
|
||||
httpAuthURL.AuthenticateURL = authurl
|
||||
emptyCookieSecret := testOptions()
|
||||
emptyCookieSecret.CookieSecret = ""
|
||||
invalidCookieSecret := testOptions()
|
||||
|
|
|
@ -3,12 +3,33 @@
|
|||
# resources to avoid being billed. For reference, this tutorial cost me <10 cents for a couple of hours.
|
||||
|
||||
# create a cluster
|
||||
gcloud container clusters create pomerium
|
||||
gcloud container clusters create pomerium --num-nodes 1
|
||||
# get cluster credentials os we can use kubctl locally
|
||||
gcloud container clusters get-credentials pomerium
|
||||
# create `pomerium` namespace
|
||||
kubectl create ns pomerium
|
||||
|
||||
######################################################################
|
||||
#### UNCOMMENT to use helm to install cert-manager & nginx-ingress####
|
||||
######################################################################
|
||||
# setup service account for tiller used by helm
|
||||
# kubectl create serviceaccount --namespace kube-system tiller
|
||||
# kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
|
||||
# helm init --service-account tiller
|
||||
# # update helm
|
||||
# helm repo update
|
||||
# kubectl get deployments -n kube-system
|
||||
# # create nginx-ingress
|
||||
# helm install --name nginx-ingress stable/nginx-ingress --set rbac.create=true
|
||||
# # install cert-manager to auto grab lets encrypt certificates
|
||||
# kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.6/deploy/manifests/00-crds.yaml
|
||||
# helm repo update
|
||||
# helm install --name cert-manager --namespace cert-manager stable/cert-manager
|
||||
# configure Let’s Encrypt Issuer
|
||||
# kubectl apply -f docs/docs/examples/kubernetes/issuer.le.prod.yml
|
||||
# kubectl apply -f docs/docs/examples/kubernetes/issuer.le.stage.yml
|
||||
# kubectl get certificate
|
||||
|
||||
# create our cryptographically random keys
|
||||
kubectl create secret generic -n pomerium shared-secret --from-literal=shared-secret=$(head -c32 /dev/urandom | base64)
|
||||
kubectl create secret generic -n pomerium cookie-secret --from-literal=cookie-secret=$(head -c32 /dev/urandom | base64)
|
||||
|
@ -23,16 +44,19 @@ kubectl create secret tls -n pomerium pomerium-tls --key privkey.pem --cert cert
|
|||
# !!! IMPORTANT !!!
|
||||
# YOU MUST CHANGE THE Identity Provider Client Secret
|
||||
# !!! IMPORTANT !!!
|
||||
# kubectl create secret generic -n pomerium idp-client-secret --from-literal=idp-client-secret=REPLACE_ME
|
||||
# kubectl create secret generic -n pomerium idp-client-secret --from-literal=REPLACE_ME
|
||||
|
||||
# Create the proxy & authenticate deployment
|
||||
kubectl create -f docs/docs/examples/kubernetes/authenticate.deploy.yml
|
||||
kubectl create -f docs/docs/examples/kubernetes/proxy.deploy.yml
|
||||
kubectl apply -f docs/docs/examples/kubernetes/authenticate.deploy.yml
|
||||
kubectl apply -f docs/docs/examples/kubernetes/proxy.deploy.yml
|
||||
# Create the proxy & authenticate services
|
||||
kubectl apply -f docs/docs/examples/kubernetes/proxy.service.yml
|
||||
kubectl apply -f docs/docs/examples/kubernetes/authenticate.service.yml
|
||||
# Create and apply the Ingress; this is GKE specific
|
||||
kubectl apply -f docs/docs/examples/kubernetes/ingress.yml
|
||||
|
||||
# Alternatively, nginx-ingress can be used
|
||||
# kubectl apply -f docs/docs/examples/kubernetes/ingress.nginx.yml
|
||||
|
||||
# When done, clean up by deleting the cluster!
|
||||
# gcloud container clusters delete pomerium
|
||||
|
|
Loading…
Add table
Reference in a new issue