authenticator: support groups (#57)

- authenticate/providers: add group support to azure
- authenticate/providers: add group support to google
- authenticate/providers: add group support to okta
- authenticate/providers: add group support to onelogin
- {authenticate/proxy}: change default cookie lifetime timeout to 14 hours
- proxy: sign group membership
- proxy: add group header
- deployment: add CHANGELOG
- deployment: fix where make release wasn’t including version
This commit is contained in:
Bobby DeSimone 2019-02-28 19:34:22 -08:00 committed by GitHub
parent a2d647ee5b
commit 1187be2bf3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1757 additions and 1706 deletions

35
CHANGELOG.md Normal file
View file

@ -0,0 +1,35 @@
# Pomerium Changelog
## Unreleased
FEATURES:
* **Group Support** : The authenticate service now retrieves a user's group membership information during authentication and refresh. This change may require additional identity provider configuration; all of which are described in the [updated docs](https://www.pomerium.io/docs/identity-providers.html). A brief summary of the requirements for each IdP are as follows:
- Google requires the [Admin SDK](https://developers.google.com/admin-sdk/directory/) to enabled, a service account with properly delegated access, and `IDP_SERVICE_ACCOUNT` to be set to the base64 encoded value of the service account's key file.
- Okta requires a `groups` claim to be added to both the `id_token` and `access_token`. No additional API calls are made.
- Microsoft Azure Active Directory requires the application be given an [additional API permission](https://docs.microsoft.com/en-us/graph/api/user-list-memberof?view=graph-rest-1.0), `Directory.Read.All`.
- Onelogin requires the [groups](https://developers.onelogin.com/openid-connect/scopes) was supplied during authentication and that groups parameter has been mapped. Group membership is validated on refresh with the [user-info api endpoint](https://developers.onelogin.com/openid-connect/api/user-info).
* **WebSocket Support** : With [Go 1.12](https://golang.org/doc/go1.12#net/http/httputil) pomerium automatically proxies WebSocket requests.
CHANGED:
* Add refresh endpoint `${url}/.pomerium/refresh` which forces a token refresh and responds with the json result.
* Group membership added to proxy headers (`x-pomerium-authenticated-user-groups`) and (`x-pomerium-jwt-assertion`).
* Default Cookie lifetime (`COOKIE_EXPIRE`) changed from 7 days to 14 hours ~ roughly one business day.
* Moved identity (`authenticate/providers`) into it's own internal identity package as third party identity providers are going to authorization details (group membership, user role, etc) in addition to just authentication attributes.
* Removed circuit breaker package. Calls that were previously wrapped with a circuit breaker fall under gRPC timeouts; which are gated by relatively short deadlines.
* Session expiration times are truncated at the second.
* **Removed gitlab provider**. We can't support groups until [this gitlab bug](https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387) is fixed.
IMPROVED:
* Request context is now maintained throughout request-flow via the [context package](https://golang.org/pkg/context/) enabling deadlines, request tracing, and cancellation.
FIXED:
*
SECURITY:
*

View file

@ -100,7 +100,7 @@ cross: ## Builds the cross-compiled binaries, creating a clean directory structu
$(foreach GOOSARCH,$(GOOSARCHES), $(call buildpretty,$(subst /,,$(dir $(GOOSARCH))),$(notdir $(GOOSARCH))))
define buildrelease
GOOS=$(1) GOARCH=$(2) CGO_ENABLED=0 GO111MODULE=on go build \
GOOS=$(1) GOARCH=$(2) CGO_ENABLED=0 GO111MODULE=on go build ${GO_LDFLAGS} \
-o $(BUILDDIR)/$(NAME)-$(1)-$(2) \
${GO_LDFLAGS_STATIC} ./cmd/$(NAME);
md5sum $(BUILDDIR)/$(NAME)-$(1)-$(2) > $(BUILDDIR)/$(NAME)-$(1)-$(2).md5;

View file

@ -11,8 +11,8 @@ import (
"github.com/pomerium/envconfig"
"github.com/pomerium/pomerium/authenticate/providers"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/identity"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/templates"
)
@ -21,7 +21,7 @@ var defaultOptions = &Options{
CookieName: "_pomerium_authenticate",
CookieHTTPOnly: true,
CookieSecure: true,
CookieExpire: time.Duration(168) * time.Hour,
CookieExpire: time.Duration(14) * time.Hour,
CookieRefresh: time.Duration(30) * time.Minute,
}
@ -55,6 +55,7 @@ type Options struct {
Provider string `envconfig:"IDP_PROVIDER"`
ProviderURL string `envconfig:"IDP_PROVIDER_URL"`
Scopes []string `envconfig:"IDP_SCOPES"`
ServiceAccount string `envconfig:"IDP_SERVICE_ACCOUNT"`
}
// OptionsFromEnvConfig builds the authenticate service's configuration environmental variables
@ -117,7 +118,7 @@ type Authenticate struct {
sessionStore sessions.SessionStore
cipher cryptutil.Cipher
provider providers.Provider
provider identity.Authenticator
}
// New validates and creates a new authenticate service from a set of Options
@ -147,15 +148,16 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
return nil, err
}
provider, err := providers.New(
provider, err := identity.New(
opts.Provider,
&providers.IdentityProvider{
&identity.Provider{
RedirectURL: opts.RedirectURL,
ProviderName: opts.Provider,
ProviderURL: opts.ProviderURL,
ClientID: opts.ClientID,
ClientSecret: opts.ClientSecret,
Scopes: opts.Scopes,
ServiceAccount: opts.ServiceAccount,
})
if err != nil {
return nil, err

View file

@ -19,7 +19,6 @@ func testOptions() *Options {
ClientSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieRefresh: time.Duration(1) * time.Hour,
// CookieLifetimeTTL: time.Duration(720) * time.Hour,
CookieExpire: time.Duration(168) * time.Hour,
CookieName: "pomerium",
}

View file

@ -1,329 +0,0 @@
// Package circuit implements the Circuit Breaker pattern.
// https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker
package circuit // import "github.com/pomerium/pomerium/internal/circuit"
import (
"fmt"
"math"
"math/rand"
"sync"
"time"
"github.com/benbjohnson/clock"
)
// State is a type that represents a state of Breaker.
type State int
// These constants are states of Breaker.
const (
StateClosed State = iota
StateHalfOpen
StateOpen
)
type (
// ShouldTripFunc is a function that takes in a Counts and returns true if the circuit breaker should be tripped.
ShouldTripFunc func(Counts) bool
// ShouldResetFunc is a function that takes in a Counts and returns true if the circuit breaker should be reset.
ShouldResetFunc func(Counts) bool
// BackoffDurationFunc is a function that takes in a Counts and returns the backoff duration
BackoffDurationFunc func(Counts) time.Duration
// StateChangeHook is a function that represents a state change.
StateChangeHook func(prev, to State)
// BackoffHook is a function that represents backoff.
BackoffHook func(duration time.Duration, reset time.Time)
)
var (
// DefaultShouldTripFunc is a default ShouldTripFunc.
DefaultShouldTripFunc = func(counts Counts) bool {
// Trip into Open after three consecutive failures
return counts.ConsecutiveFailures >= 3
}
// DefaultShouldResetFunc is a default ShouldResetFunc.
DefaultShouldResetFunc = func(counts Counts) bool {
// Reset after three consecutive successes
return counts.ConsecutiveSuccesses >= 3
}
// DefaultBackoffDurationFunc is an exponential backoff function
DefaultBackoffDurationFunc = ExponentialBackoffDuration(time.Duration(100)*time.Second, time.Duration(500)*time.Millisecond)
)
// ErrOpenState is returned when the b state is open
type ErrOpenState struct{}
func (e *ErrOpenState) Error() string { return "circuit breaker is open" }
// ExponentialBackoffDuration returns a function that uses exponential backoff and full jitter
func ExponentialBackoffDuration(maxBackoff, baseTimeout time.Duration) func(Counts) time.Duration {
return func(counts Counts) time.Duration {
// Full Jitter from https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
// sleep = random_between(0, min(cap, base * 2 ** attempt))
backoff := math.Min(float64(maxBackoff), float64(baseTimeout)*math.Exp2(float64(counts.ConsecutiveFailures)))
jittered := rand.Float64() * backoff
return time.Duration(jittered)
}
}
// String implements stringer interface.
func (s State) String() string {
switch s {
case StateClosed:
return "closed"
case StateHalfOpen:
return "half-open"
case StateOpen:
return "open"
default:
return fmt.Sprintf("unknown state: %d", s)
}
}
// Counts holds the numbers of requests and their successes/failures.
type Counts struct {
CurrentRequests int
ConsecutiveSuccesses int
ConsecutiveFailures int
}
func (c *Counts) onRequest() {
c.CurrentRequests++
}
func (c *Counts) afterRequest() {
c.CurrentRequests--
}
func (c *Counts) onSuccess() {
c.ConsecutiveSuccesses++
c.ConsecutiveFailures = 0
}
func (c *Counts) onFailure() {
c.ConsecutiveFailures++
c.ConsecutiveSuccesses = 0
}
func (c *Counts) clear() {
c.ConsecutiveSuccesses = 0
c.ConsecutiveFailures = 0
}
// Options configures Breaker:
//
// HalfOpenConcurrentRequests specifies how many concurrent requests to allow while
// the circuit is in the half-open state
//
// ShouldTripFunc specifies when the circuit should trip from the closed state to
// the open state. It takes a Counts struct and returns a bool.
//
// ShouldResetFunc specifies when the circuit should be reset from the half-open state
// to the closed state and allow all requests. It takes a Counts struct and returns a bool.
//
// BackoffDurationFunc specifies how long to set the backoff duration. It takes a
// counts struct and returns a time.Duration
//
// OnStateChange is called whenever the state of the Breaker changes.
//
// OnBackoff is called whenever a backoff is set with the backoff duration and reset time
//
// TestClock is used to mock the clock during tests
type Options struct {
HalfOpenConcurrentRequests int
ShouldTripFunc ShouldTripFunc
ShouldResetFunc ShouldResetFunc
BackoffDurationFunc BackoffDurationFunc
// hooks
OnStateChange StateChangeHook
OnBackoff BackoffHook
// used in tests
TestClock clock.Clock
}
// Breaker is a state machine to prevent sending requests that are likely to fail.
type Breaker struct {
halfOpenRequests int
shouldTripFunc ShouldTripFunc
shouldResetFunc ShouldResetFunc
backoffDurationFunc BackoffDurationFunc
// hooks
onStateChange StateChangeHook
onBackoff BackoffHook
// used primarily for mocking tests
clock clock.Clock
mutex sync.Mutex
state State
counts Counts
backoffExpires time.Time
generation int
}
// NewBreaker returns a new Breaker configured with the given Settings.
func NewBreaker(opts *Options) *Breaker {
b := new(Breaker)
if opts == nil {
opts = &Options{}
}
// set hooks
b.onStateChange = opts.OnStateChange
b.onBackoff = opts.OnBackoff
b.halfOpenRequests = 1
if opts.HalfOpenConcurrentRequests > 0 {
b.halfOpenRequests = opts.HalfOpenConcurrentRequests
}
b.backoffDurationFunc = DefaultBackoffDurationFunc
if opts.BackoffDurationFunc != nil {
b.backoffDurationFunc = opts.BackoffDurationFunc
}
b.shouldTripFunc = DefaultShouldTripFunc
if opts.ShouldTripFunc != nil {
b.shouldTripFunc = opts.ShouldTripFunc
}
b.shouldResetFunc = DefaultShouldResetFunc
if opts.ShouldResetFunc != nil {
b.shouldResetFunc = opts.ShouldResetFunc
}
b.clock = clock.New()
if opts.TestClock != nil {
b.clock = opts.TestClock
}
b.setState(StateClosed)
return b
}
// Call runs the given function if the Breaker allows the call.
// Call returns an error instantly if the Breaker rejects the request.
// Otherwise, Call returns the result of the request.
func (b *Breaker) Call(f func() (interface{}, error)) (interface{}, error) {
generation, err := b.beforeRequest()
if err != nil {
return nil, err
}
result, err := f()
b.afterRequest(err == nil, generation)
return result, err
}
func (b *Breaker) beforeRequest() (int, error) {
b.mutex.Lock()
defer b.mutex.Unlock()
state, generation := b.currentState()
switch state {
case StateOpen:
return generation, &ErrOpenState{}
case StateHalfOpen:
if b.counts.CurrentRequests >= b.halfOpenRequests {
return generation, &ErrOpenState{}
}
}
b.counts.onRequest()
return generation, nil
}
func (b *Breaker) afterRequest(success bool, prevGeneration int) {
b.mutex.Lock()
defer b.mutex.Unlock()
b.counts.afterRequest()
state, generation := b.currentState()
if prevGeneration != generation {
return
}
if success {
b.onSuccess(state)
return
}
b.onFailure(state)
}
func (b *Breaker) onSuccess(state State) {
b.counts.onSuccess()
switch state {
case StateHalfOpen:
if b.shouldResetFunc(b.counts) {
b.setState(StateClosed)
b.counts.clear()
}
}
}
func (b *Breaker) onFailure(state State) {
b.counts.onFailure()
switch state {
case StateClosed:
if b.shouldTripFunc(b.counts) {
b.setState(StateOpen)
b.counts.clear()
b.setBackoff()
}
case StateOpen:
b.setBackoff()
case StateHalfOpen:
b.setState(StateOpen)
b.setBackoff()
}
}
func (b *Breaker) setBackoff() {
backoffDuration := b.backoffDurationFunc(b.counts)
backoffExpires := b.clock.Now().Add(backoffDuration)
b.backoffExpires = backoffExpires
if b.onBackoff != nil {
b.onBackoff(backoffDuration, backoffExpires)
}
}
func (b *Breaker) currentState() (State, int) {
switch b.state {
case StateOpen:
if b.clock.Now().After(b.backoffExpires) {
b.setState(StateHalfOpen)
}
}
return b.state, b.generation
}
func (b *Breaker) newGeneration() {
b.generation++
}
func (b *Breaker) setState(state State) {
if b.state == state {
return
}
b.newGeneration()
prev := b.state
b.state = state
if b.onStateChange != nil {
b.onStateChange(prev, state)
}
}

View file

@ -1,187 +0,0 @@
package circuit // import "github.com/pomerium/pomerium/internal/circuit"
import (
"errors"
"sync"
"testing"
"time"
"github.com/benbjohnson/clock"
)
var errFailed = errors.New("failed")
func fail() (interface{}, error) {
return nil, errFailed
}
func succeed() (interface{}, error) {
return nil, nil
}
func TestCircuitBreaker(t *testing.T) {
mock := clock.NewMock()
threshold := 3
timeout := time.Duration(2) * time.Second
trip := func(c Counts) bool { return c.ConsecutiveFailures > threshold }
reset := func(c Counts) bool { return c.ConsecutiveSuccesses > threshold }
backoff := func(c Counts) time.Duration { return timeout }
stateChange := func(p, c State) { t.Logf("state change from %s to %s\n", p, c) }
cb := NewBreaker(&Options{
TestClock: mock,
ShouldTripFunc: trip,
ShouldResetFunc: reset,
BackoffDurationFunc: backoff,
OnStateChange: stateChange,
})
state, _ := cb.currentState()
if state != StateClosed {
t.Fatalf("expected state to start %s, got %s", StateClosed, state)
}
for i := 0; i <= threshold; i++ {
_, err := cb.Call(fail)
if err == nil {
t.Fatalf("expected to error, got nil")
}
state, _ := cb.currentState()
t.Logf("iteration %#v", i)
if i == threshold {
// we expect this to be the case to trip the circuit
if state != StateOpen {
t.Fatalf("expected state to be %s, got %s", StateOpen, state)
}
} else if state != StateClosed {
// this is a normal failure case
t.Fatalf("expected state to be %s, got %s", StateClosed, state)
}
}
_, err := cb.Call(fail)
switch err.(type) {
case *ErrOpenState:
// this is the expected case
break
default:
t.Errorf("%#v", cb.counts)
t.Fatalf("expected to get open state failure, got %s", err)
}
// we advance time by the timeout and a hair
mock.Add(timeout + time.Duration(1)*time.Millisecond)
state, _ = cb.currentState()
if state != StateHalfOpen {
t.Fatalf("expected state to be %s, got %s", StateHalfOpen, state)
}
for i := 0; i <= threshold; i++ {
_, err := cb.Call(succeed)
if err != nil {
t.Fatalf("expected to get no error, got %s", err)
}
state, _ := cb.currentState()
t.Logf("iteration %#v", i)
if i == threshold {
// we expect this to be the case that ressets the circuit
if state != StateClosed {
t.Fatalf("expected state to be %s, got %s", StateClosed, state)
}
} else if state != StateHalfOpen {
t.Fatalf("expected state to be %s, got %s", StateHalfOpen, state)
}
}
state, _ = cb.currentState()
if state != StateClosed {
t.Fatalf("expected state to be %s, got %s", StateClosed, state)
}
}
func TestExponentialBackOffFunc(t *testing.T) {
baseTimeout := time.Duration(1) * time.Millisecond
// Note Expected is an upper range case
cases := []struct {
FailureCount int
Expected time.Duration
}{
{
FailureCount: 0,
Expected: time.Duration(1) * time.Millisecond,
},
{
FailureCount: 1,
Expected: time.Duration(2) * time.Millisecond,
},
{
FailureCount: 2,
Expected: time.Duration(4) * time.Millisecond,
},
{
FailureCount: 3,
Expected: time.Duration(8) * time.Millisecond,
},
{
FailureCount: 4,
Expected: time.Duration(16) * time.Millisecond,
},
{
FailureCount: 5,
Expected: time.Duration(32) * time.Millisecond,
},
{
FailureCount: 6,
Expected: time.Duration(64) * time.Millisecond,
},
{
FailureCount: 7,
Expected: time.Duration(128) * time.Millisecond,
},
{
FailureCount: 8,
Expected: time.Duration(256) * time.Millisecond,
},
{
FailureCount: 9,
Expected: time.Duration(512) * time.Millisecond,
},
{
FailureCount: 10,
Expected: time.Duration(1024) * time.Millisecond,
},
}
f := ExponentialBackoffDuration(time.Duration(1)*time.Hour, baseTimeout)
for _, tc := range cases {
got := f(Counts{ConsecutiveFailures: tc.FailureCount})
t.Logf("got backoff %#v", got)
if got > tc.Expected {
t.Errorf("got %#v but expected less than %#v", got, tc.Expected)
}
}
}
func TestCircuitBreakerClosedParallel(t *testing.T) {
cb := NewBreaker(nil)
numReqs := 10000
wg := &sync.WaitGroup{}
routine := func(wg *sync.WaitGroup) {
for i := 0; i < numReqs; i++ {
cb.Call(succeed)
}
wg.Done()
}
numRoutines := 10
for i := 0; i < numRoutines; i++ {
wg.Add(1)
go routine(wg)
}
total := numReqs * numRoutines
wg.Wait()
if cb.counts.ConsecutiveSuccesses != total {
t.Fatalf("expected to get total requests %d, got %d", total, cb.counts.ConsecutiveSuccesses)
}
}

View file

@ -3,62 +3,54 @@ 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) {
func (p *Authenticate) Authenticate(ctx context.Context, in *pb.AuthenticateRequest) (*pb.Session, error) {
session, err := sessions.UnmarshalSession(in.Code, p.cipher)
if err != nil {
return nil, fmt.Errorf("authenticate/grpc: %v", err)
return nil, fmt.Errorf("authenticate/grpc: authenticate %v", err)
}
expiryTimestamp, err := ptypes.TimestampProto(session.RefreshDeadline)
newSessionProto, err := pb.ProtoFromSession(session)
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
return newSessionProto, 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)
isValid, err := p.provider.Validate(ctx, in.IdToken)
if err != nil {
return &pb.ValidateReply{IsValid: false}, err
return &pb.ValidateReply{IsValid: false}, fmt.Errorf("authenticate/grpc: validate %v", 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)
func (p *Authenticate) Refresh(ctx context.Context, in *pb.Session) (*pb.Session, error) {
// todo(bdd): add request id from incoming context
// md, _ := metadata.FromIncomingContext(ctx)
// sublogger := log.With().Str("req_id", md.Get("req_id")[0]).WithContext(ctx)
// sublogger.Info().Msg("tracing sucks!")
if in == nil {
return nil, fmt.Errorf("authenticate/grpc: session cannot be nil")
}
oldSession, err := pb.SessionFromProto(in)
if err != nil {
return nil, err
}
expiryTimestamp, err := ptypes.TimestampProto(newToken.Expiry)
newSession, err := p.provider.Refresh(ctx, oldSession)
if err != nil {
return nil, fmt.Errorf("authenticate/grpc: refresh failed %v", err)
}
newSessionProto, err := pb.ProtoFromSession(newSession)
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
return newSessionProto, nil
}

View file

@ -7,58 +7,32 @@ import (
"testing"
"time"
"github.com/pomerium/pomerium/internal/identity"
"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
mp *identity.MockProvider
want bool
wantErr bool
}{
{"good", "example", false, false},
{"error", "error", false, true},
{"not error", "not error", false, false},
{"good", "example", &identity.MockProvider{}, false, false},
{"error", "error", &identity.MockProvider{ValidateError: errors.New("err")}, false, true},
{"not error", "not error", &identity.MockProvider{ValidateError: nil}, false, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tp := &testProvider{}
p := &Authenticate{provider: tp}
p := &Authenticate{provider: tt.mp}
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)
@ -79,23 +53,42 @@ func TestAuthenticate_Refresh(t *testing.T) {
tests := []struct {
name string
refreshToken string
want *pb.RefreshReply
mock *identity.MockProvider
originalSession *pb.Session
want *pb.Session
wantErr bool
}{
{"good", "refresh-token", &pb.RefreshReply{AccessToken: "updated", Expiry: fixedProtoTime}, false},
{"test error", "error", nil, true},
{"good",
&identity.MockProvider{
RefreshResponse: &sessions.SessionState{
AccessToken: "updated",
LifetimeDeadline: fixedDate,
RefreshDeadline: fixedDate,
}},
&pb.Session{
AccessToken: "original",
LifetimeDeadline: fixedProtoTime,
RefreshDeadline: fixedProtoTime,
},
&pb.Session{
AccessToken: "updated",
LifetimeDeadline: fixedProtoTime,
RefreshDeadline: fixedProtoTime,
},
false},
{"test error", &identity.MockProvider{RefreshError: errors.New("hi")}, &pb.Session{RefreshToken: "refresh token", RefreshDeadline: fixedProtoTime, LifetimeDeadline: fixedProtoTime}, nil, true},
{"test catch nil", nil, nil, nil, true},
// {"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}
p := &Authenticate{provider: tt.mock}
got, err := p.Refresh(context.Background(), &pb.RefreshRequest{RefreshToken: tt.refreshToken})
got, err := p.Refresh(context.Background(), tt.originalSession)
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)
@ -132,10 +125,11 @@ func TestAuthenticate_Authenticate(t *testing.T) {
User: "user",
}
goodReply := &pb.AuthenticateReply{
goodReply := &pb.Session{
AccessToken: "token1234",
RefreshToken: "refresh4321",
Expiry: vtProto,
LifetimeDeadline: vtProto,
RefreshDeadline: vtProto,
Email: "user@domain.com",
User: "user"}
ciphertext, err := sessions.MarshalSession(want, c)
@ -147,7 +141,7 @@ func TestAuthenticate_Authenticate(t *testing.T) {
name string
cipher cryptutil.Cipher
code string
want *pb.AuthenticateReply
want *pb.Session
wantErr bool
}{
{"good", c, ciphertext, goodReply, false},
@ -162,7 +156,7 @@ func TestAuthenticate_Authenticate(t *testing.T) {
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Authenticate.Authenticate() = %v, want %v", got, tt.want)
t.Errorf("Authenticate.Authenticate() = got: \n%vwant:\n%v", got, tt.want)
}
})
}

View file

@ -83,34 +83,29 @@ func (a *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*se
// check if session refresh period is up
if session.RefreshPeriodExpired() {
newToken, err := a.provider.Refresh(session.RefreshToken)
newSession, err := a.provider.Refresh(r.Context(), session)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed to refresh session")
a.sessionStore.ClearSession(w, r)
return nil, err
}
session.AccessToken = newToken.AccessToken
session.RefreshDeadline = newToken.Expiry
err = a.sessionStore.SaveSession(w, r, session)
err = a.sessionStore.SaveSession(w, r, newSession)
if err != nil {
// We refreshed the session successfully, but failed to save it.
// This could be from failing to encode the session properly.
// But, we clear the session cookie and reject the request
log.FromRequest(r).Error().Err(err).Msg("could not save refreshed session")
log.FromRequest(r).Error().Err(err).Msg("authenticate: could not save refreshed session")
a.sessionStore.ClearSession(w, r)
return nil, err
}
} else {
// The session has not exceeded it's lifetime or requires refresh
ok, err := a.provider.Validate(session.IDToken)
ok, err := a.provider.Validate(r.Context(), session.IDToken)
if !ok || err != nil {
log.FromRequest(r).Error().Err(err).Msg("invalid session state")
log.FromRequest(r).Error().Err(err).Msg("authenticate: invalid session state")
a.sessionStore.ClearSession(w, r)
return nil, httputil.ErrUserNotAuthorized
}
err = a.sessionStore.SaveSession(w, r, session)
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("failed to save valid session")
log.FromRequest(r).Error().Err(err).Msg("authenticate: failed to save valid session")
a.sessionStore.ClearSession(w, r)
return nil, err
}
@ -136,7 +131,6 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) {
}
log.FromRequest(r).Info().Msg("authenticate: user authenticated")
a.ProxyCallback(w, r, session)
}
// ProxyCallback redirects the user back to proxy service along with an encrypted payload, as
@ -310,7 +304,7 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
}
errorString := r.Form.Get("error")
if errorString != "" {
log.FromRequest(r).Error().Err(err).Msg("authenticate: provider returned error")
log.FromRequest(r).Error().Str("Error", errorString).Msg("authenticate: provider returned error")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: errorString}
}
code := r.Form.Get("code")

View file

@ -11,11 +11,10 @@ import (
"testing"
"time"
"github.com/pomerium/pomerium/authenticate/providers"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/identity"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/templates"
"golang.org/x/oauth2"
)
// mocks for validator func
@ -89,36 +88,36 @@ func TestAuthenticate_authenticate(t *testing.T) {
tests := []struct {
name string
session sessions.SessionStore
provider providers.MockProvider
provider identity.MockProvider
validator func(string) bool
want *sessions.SessionState
wantErr bool
}{
{"good", goodSession, providers.MockProvider{ValidateResponse: true}, trueValidator, nil, false},
{"good but fails validation", goodSession, providers.MockProvider{ValidateResponse: true}, falseValidator, nil, true},
{"can't load session", &sessions.MockSessionStore{LoadError: errors.New("error")}, providers.MockProvider{ValidateResponse: true}, trueValidator, nil, true},
{"validation fails", goodSession, providers.MockProvider{ValidateResponse: false}, trueValidator, nil, true},
{"good", goodSession, identity.MockProvider{ValidateResponse: true}, trueValidator, nil, false},
{"good but fails validation", goodSession, identity.MockProvider{ValidateResponse: true}, falseValidator, nil, true},
{"can't load session", &sessions.MockSessionStore{LoadError: errors.New("error")}, identity.MockProvider{ValidateResponse: true}, trueValidator, nil, true},
{"validation fails", goodSession, identity.MockProvider{ValidateResponse: false}, trueValidator, nil, true},
{"session fails after good validation", &sessions.MockSessionStore{
SaveError: errors.New("error"),
Session: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * time.Second),
}}, providers.MockProvider{ValidateResponse: true},
}}, identity.MockProvider{ValidateResponse: true},
trueValidator, nil, true},
{"refresh expired",
expiredRefresPeriod,
providers.MockProvider{
identity.MockProvider{
ValidateResponse: true,
RefreshResponse: &oauth2.Token{
RefreshResponse: &sessions.SessionState{
AccessToken: "new token",
Expiry: time.Now(),
LifetimeDeadline: time.Now(),
},
},
trueValidator, nil, false},
{"refresh expired refresh error",
expiredRefresPeriod,
providers.MockProvider{
identity.MockProvider{
ValidateResponse: true,
RefreshError: errors.New("error"),
},
@ -132,11 +131,11 @@ func TestAuthenticate_authenticate(t *testing.T) {
RefreshDeadline: time.Now().Add(10 * -time.Second),
}},
providers.MockProvider{
identity.MockProvider{
ValidateResponse: true,
RefreshResponse: &oauth2.Token{
RefreshResponse: &sessions.SessionState{
AccessToken: "new token",
Expiry: time.Now(),
LifetimeDeadline: time.Now(),
},
},
trueValidator, nil, true},
@ -164,7 +163,7 @@ func TestAuthenticate_SignIn(t *testing.T) {
tests := []struct {
name string
session sessions.SessionStore
provider providers.MockProvider
provider identity.MockProvider
validator func(string) bool
wantCode int
}{
@ -175,7 +174,7 @@ func TestAuthenticate_SignIn(t *testing.T) {
RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * time.Second),
}},
providers.MockProvider{ValidateResponse: true},
identity.MockProvider{ValidateResponse: true},
trueValidator,
http.StatusForbidden},
{"session fails after good validation", &sessions.MockSessionStore{
@ -184,7 +183,7 @@ func TestAuthenticate_SignIn(t *testing.T) {
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * time.Second),
}}, providers.MockProvider{ValidateResponse: true},
}}, identity.MockProvider{ValidateResponse: true},
trueValidator, http.StatusBadRequest},
}
for _, tt := range tests {
@ -359,7 +358,7 @@ func TestAuthenticate_SignOut(t *testing.T) {
sig string
ts string
provider providers.Provider
provider identity.Authenticator
sessionStore sessions.SessionStore
wantCode int
wantBody string
@ -369,7 +368,7 @@ func TestAuthenticate_SignOut(t *testing.T) {
"https://corp.pomerium.io/",
"sig",
"ts",
providers.MockProvider{},
identity.MockProvider{},
&sessions.MockSessionStore{
Session: &sessions.SessionState{
AccessToken: "AccessToken",
@ -386,7 +385,7 @@ func TestAuthenticate_SignOut(t *testing.T) {
"https://corp.pomerium.io/",
"sig",
"ts",
providers.MockProvider{RevokeError: errors.New("OH NO")},
identity.MockProvider{RevokeError: errors.New("OH NO")},
&sessions.MockSessionStore{
Session: &sessions.SessionState{
AccessToken: "AccessToken",
@ -404,7 +403,7 @@ func TestAuthenticate_SignOut(t *testing.T) {
"https://corp.pomerium.io/",
"sig",
"ts",
providers.MockProvider{},
identity.MockProvider{},
&sessions.MockSessionStore{
Session: &sessions.SessionState{
AccessToken: "AccessToken",
@ -421,7 +420,7 @@ func TestAuthenticate_SignOut(t *testing.T) {
"https://corp.pomerium.io/",
"sig",
"ts",
providers.MockProvider{},
identity.MockProvider{},
&sessions.MockSessionStore{
LoadError: errors.New("uh oh"),
Session: &sessions.SessionState{
@ -439,7 +438,7 @@ func TestAuthenticate_SignOut(t *testing.T) {
"https://pomerium.com%zzzzz",
"sig",
"ts",
providers.MockProvider{},
identity.MockProvider{},
&sessions.MockSessionStore{
Session: &sessions.SessionState{
AccessToken: "AccessToken",
@ -497,7 +496,7 @@ func TestAuthenticate_OAuthStart(t *testing.T) {
ts string
allowedDomains []string
provider providers.Provider
provider identity.Authenticator
csrfStore sessions.MockCSRFStore
// sessionStore sessions.SessionStore
wantCode int
@ -508,7 +507,7 @@ func TestAuthenticate_OAuthStart(t *testing.T) {
redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"),
fmt.Sprint(time.Now().Unix()),
[]string{".pomerium.io"},
providers.MockProvider{},
identity.MockProvider{},
sessions.MockCSRFStore{},
http.StatusFound,
},
@ -518,7 +517,7 @@ func TestAuthenticate_OAuthStart(t *testing.T) {
redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"),
fmt.Sprint(time.Now().Add(10 * time.Hour).Unix()),
[]string{".pomerium.io"},
providers.MockProvider{},
identity.MockProvider{},
sessions.MockCSRFStore{},
http.StatusBadRequest,
},
@ -528,7 +527,7 @@ func TestAuthenticate_OAuthStart(t *testing.T) {
redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"),
fmt.Sprint(time.Now().Unix()),
[]string{"not.pomerium.io"},
providers.MockProvider{},
identity.MockProvider{},
sessions.MockCSRFStore{},
http.StatusBadRequest,
},
@ -538,7 +537,7 @@ func TestAuthenticate_OAuthStart(t *testing.T) {
redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"),
fmt.Sprint(time.Now().Unix()),
[]string{".pomerium.io"},
providers.MockProvider{},
identity.MockProvider{},
sessions.MockCSRFStore{},
http.StatusBadRequest,
},
@ -548,7 +547,7 @@ func TestAuthenticate_OAuthStart(t *testing.T) {
redirectURLSignature("https://corp.pomerium.io/", time.Now(), "secret"),
fmt.Sprint(time.Now().Unix()),
[]string{".pomerium.io"},
providers.MockProvider{},
identity.MockProvider{},
sessions.MockCSRFStore{},
http.StatusBadRequest,
},
@ -596,7 +595,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
validator func(string) bool
session sessions.SessionStore
provider providers.MockProvider
provider identity.MockProvider
csrfStore sessions.MockCSRFStore
want string
@ -610,7 +609,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
providers.MockProvider{
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
@ -632,7 +631,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
providers.MockProvider{
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
@ -655,7 +654,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
providers.MockProvider{
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
@ -677,7 +676,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
providers.MockProvider{
identity.MockProvider{
AuthenticateError: errors.New("error"),
},
sessions.MockCSRFStore{
@ -694,7 +693,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{SaveError: errors.New("error")},
providers.MockProvider{
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
@ -716,7 +715,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
[]string{"pomerium.io"},
falseValidator,
&sessions.MockSessionStore{},
providers.MockProvider{
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
@ -739,7 +738,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
providers.MockProvider{
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
@ -761,7 +760,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
providers.MockProvider{
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
@ -783,7 +782,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
providers.MockProvider{
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
@ -805,7 +804,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
providers.MockProvider{
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
@ -827,7 +826,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
providers.MockProvider{
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",

View file

@ -1,5 +0,0 @@
// Package providers authentication for third party identity providers (IdP) using OpenID
// Connect, an identity layer on top of the OAuth 2.0 RFC6749 protocol.
//
// see: https://openid.net/specs/openid-connect-core-1_0.html
package providers // import "github.com/pomerium/pomerium/internal/providers"

View file

@ -1,80 +0,0 @@
package providers // import "github.com/pomerium/pomerium/internal/providers"
import (
"context"
"time"
oidc "github.com/pomerium/go-oidc"
"golang.org/x/oauth2"
"github.com/pomerium/pomerium/authenticate/circuit"
"github.com/pomerium/pomerium/internal/log"
)
const defaultGitlabProviderURL = "https://gitlab.com"
// GitlabProvider is an implementation of the Provider interface.
type GitlabProvider struct {
*IdentityProvider
cb *circuit.Breaker
}
// NewGitlabProvider returns a new Gitlab identity provider; defaults to the hosted version.
//
// Unlike other providers, `email` is not returned from the initial OIDC token. To retrieve email,
// a secondary call must be made to the user's info endpoint. Unfortunately, email is not guaranteed
// or even likely to be returned even if the user has it set as their email must be set to public.
// As pomerium is currently very email centric, I would caution using until Gitlab fixes the issue.
//
// See :
// - https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387
// - https://docs.gitlab.com/ee/integration/openid_connect_provider.html
// - 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 *IdentityProvider) (*GitlabProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
p.ProviderURL = defaultGitlabProviderURL
}
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
if err != nil {
return nil, err
}
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,
ClientSecret: p.ClientSecret,
Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(),
Scopes: p.Scopes,
}
gitlabProvider := &GitlabProvider{
IdentityProvider: p,
}
gitlabProvider.cb = circuit.NewBreaker(&circuit.Options{
HalfOpenConcurrentRequests: 2,
OnStateChange: gitlabProvider.cbStateChange,
OnBackoff: gitlabProvider.cbBackoff,
ShouldTripFunc: func(c circuit.Counts) bool { return c.ConsecutiveFailures >= 3 },
ShouldResetFunc: func(c circuit.Counts) bool { return c.ConsecutiveSuccesses >= 6 },
BackoffDurationFunc: circuit.ExponentialBackoffDuration(
time.Duration(200)*time.Second,
time.Duration(500)*time.Millisecond),
})
return gitlabProvider, nil
}
func (p *GitlabProvider) cbBackoff(duration time.Duration, reset time.Time) {
log.Info().Dur("duration", duration).Msg("authenticate/providers/gitlab.cbBackoff")
}
func (p *GitlabProvider) cbStateChange(from, to circuit.State) {
log.Info().Str("from", from.String()).Str("to", to.String()).Msg("authenticate/providers/gitlab.cbStateChange")
}

View file

@ -1,110 +0,0 @@
package providers // import "github.com/pomerium/pomerium/internal/providers"
import (
"context"
"net/url"
"time"
oidc "github.com/pomerium/go-oidc"
"golang.org/x/oauth2"
"github.com/pomerium/pomerium/authenticate/circuit"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/version"
)
const defaultGoogleProviderURL = "https://accounts.google.com"
// GoogleProvider is an implementation of the Provider interface.
type GoogleProvider struct {
*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 *IdentityProvider) (*GoogleProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
p.ProviderURL = defaultGoogleProviderURL
}
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
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: p.Scopes,
}
googleProvider := &GoogleProvider{
IdentityProvider: p,
}
// google supports a revocation endpoint
var claims struct {
RevokeURL string `json:"revocation_endpoint"`
}
if err := p.provider.Claims(&claims); err != nil {
return nil, err
}
googleProvider.RevokeURL, err = url.Parse(claims.RevokeURL)
if err != nil {
return nil, err
}
googleProvider.cb = circuit.NewBreaker(&circuit.Options{
HalfOpenConcurrentRequests: 2,
OnStateChange: googleProvider.cbStateChange,
OnBackoff: googleProvider.cbBackoff,
ShouldTripFunc: func(c circuit.Counts) bool { return c.ConsecutiveFailures >= 3 },
ShouldResetFunc: func(c circuit.Counts) bool { return c.ConsecutiveSuccesses >= 6 },
BackoffDurationFunc: circuit.ExponentialBackoffDuration(
time.Duration(200)*time.Second,
time.Duration(500)*time.Millisecond),
})
return googleProvider, nil
}
func (p *GoogleProvider) cbBackoff(duration time.Duration, reset time.Time) {
log.Info().Dur("duration", duration).Msg("authenticate/providers/google.cbBackoff")
}
func (p *GoogleProvider) cbStateChange(from, to circuit.State) {
log.Info().Str("from", from.String()).Str("to", to.String()).Msg("authenticate/providers/google.cbStateChange")
}
// Revoke revokes the access token a given session state.
//
// https://developers.google.com/identity/protocols/OAuth2WebServer#tokenrevoke
// https://github.com/googleapis/google-api-dotnet-client/issues/1285
func (p *GoogleProvider) Revoke(accessToken string) error {
params := url.Values{}
params.Add("token", accessToken)
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil)
if err != nil && err != httputil.ErrTokenRevoked {
return err
}
return nil
}
// GetSignInURL returns the sign in url with typical oauth parameters
// Google requires access type offline
func (p *GoogleProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
}

View file

@ -1,112 +0,0 @@
package providers // import "github.com/pomerium/pomerium/internal/providers"
import (
"context"
"net/url"
"time"
oidc "github.com/pomerium/go-oidc"
"golang.org/x/oauth2"
"github.com/pomerium/pomerium/authenticate/circuit"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/version"
)
// defaultAzureProviderURL Users with both a personal Microsoft
// account and a work or school account from Azure Active Directory (Azure AD)
// an sign in to the application.
const defaultAzureProviderURL = "https://login.microsoftonline.com/common"
// AzureProvider is an implementation of the Provider interface
type AzureProvider struct {
*IdentityProvider
cb *circuit.Breaker
// non-standard oidc fields
RevokeURL *url.URL
}
// 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 *IdentityProvider) (*AzureProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
p.ProviderURL = defaultAzureProviderURL
}
log.Info().Msgf("provider url %s", p.ProviderURL)
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
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: p.Scopes,
}
azureProvider := &AzureProvider{
IdentityProvider: p,
}
// azure has a "end session endpoint"
var claims struct {
RevokeURL string `json:"end_session_endpoint"`
}
if err := p.provider.Claims(&claims); err != nil {
return nil, err
}
azureProvider.RevokeURL, err = url.Parse(claims.RevokeURL)
if err != nil {
return nil, err
}
azureProvider.cb = circuit.NewBreaker(&circuit.Options{
HalfOpenConcurrentRequests: 2,
OnStateChange: azureProvider.cbStateChange,
OnBackoff: azureProvider.cbBackoff,
ShouldTripFunc: func(c circuit.Counts) bool { return c.ConsecutiveFailures >= 3 },
ShouldResetFunc: func(c circuit.Counts) bool { return c.ConsecutiveSuccesses >= 6 },
BackoffDurationFunc: circuit.ExponentialBackoffDuration(
time.Duration(200)*time.Second,
time.Duration(500)*time.Millisecond),
})
return azureProvider, nil
}
func (p *AzureProvider) cbBackoff(duration time.Duration, reset time.Time) {
log.Info().Dur("duration", duration).Msg("authenticate/providers/azure.cbBackoff")
}
func (p *AzureProvider) cbStateChange(from, to circuit.State) {
log.Info().Str("from", from.String()).Str("to", to.String()).Msg("authenticate/providers/azure.cbStateChange")
}
// 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(token string) error {
params := url.Values{}
params.Add("token", token)
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil)
if err != nil && err != httputil.ErrTokenRevoked {
return err
}
return nil
}
// GetSignInURL returns the sign in url with typical oauth parameters
func (p *AzureProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
}

View file

@ -1,81 +0,0 @@
package providers // import "github.com/pomerium/pomerium/internal/providers"
import (
"context"
"net/url"
oidc "github.com/pomerium/go-oidc"
"golang.org/x/oauth2"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/version"
)
// OktaProvider provides a standard, OpenID Connect implementation
// of an authorization identity provider.
type OktaProvider struct {
*IdentityProvider
// non-standard oidc fields
RevokeURL *url.URL
}
// NewOktaProvider creates a new instance of an OpenID Connect provider.
func NewOktaProvider(p *IdentityProvider) (*OktaProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
return nil, ErrMissingProviderURL
}
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
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: p.Scopes,
}
// 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 {
return nil, err
}
return &oktaProvider, nil
}
// Revoke revokes the access token a given session state.
// https://developer.okta.com/docs/api/resources/oidc#revoke
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", 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 {
return err
}
return nil
}
// GetSignInURL returns the sign in url with typical oauth parameters
// Google requires access type offline
func (p *OktaProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline)
}

View file

@ -1,84 +0,0 @@
package providers // import "github.com/pomerium/pomerium/internal/providers"
import (
"context"
"net/url"
oidc "github.com/pomerium/go-oidc"
"golang.org/x/oauth2"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/version"
)
// OneLoginProvider provides a standard, OpenID Connect implementation
// of an authorization identity provider.
type OneLoginProvider struct {
*IdentityProvider
// non-standard oidc fields
RevokeURL *url.URL
}
const defaultOneLoginProviderURL = "https://openid-connect.onelogin.com/oidc"
// NewOneLoginProvider creates a new instance of an OpenID Connect provider.
func NewOneLoginProvider(p *IdentityProvider) (*OneLoginProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
p.ProviderURL = defaultOneLoginProviderURL
}
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
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: p.Scopes,
}
// okta supports a revocation endpoint
var claims struct {
RevokeURL string `json:"revocation_endpoint"`
}
if err := p.provider.Claims(&claims); err != nil {
return nil, err
}
OneLoginProvider := OneLoginProvider{IdentityProvider: p}
OneLoginProvider.RevokeURL, err = url.Parse(claims.RevokeURL)
if err != nil {
return nil, err
}
return &OneLoginProvider, nil
}
// Revoke revokes the access token a given session state.
// https://developers.onelogin.com/openid-connect/api/revoke-session
func (p *OneLoginProvider) Revoke(token string) error {
params := url.Values{}
params.Add("client_id", p.ClientID)
params.Add("client_secret", p.ClientSecret)
params.Add("token", token)
params.Add("token_type_hint", "access_token")
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), params, nil)
if err != nil && err != httputil.ErrTokenRevoked {
log.Error().Err(err).Msg("authenticate/providers: failed to revoke session")
return err
}
return nil
}
// GetSignInURL returns the sign in url with typical oauth parameters
func (p *OneLoginProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View file

@ -10,9 +10,11 @@ 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 IdentityProvider. 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 identity and authentication interface.
1. Establish a **Redirect URL** with the identity provider which is called after IdentityProvider.
In this guide we'll cover how to do the following for each identity provider:
1. Set a **[Redirect URL]** pointing back to Pomerium.
2. Generate a **[Client ID]** and **[Client Secret]**.
3. Configure Pomerium to use the **[Client ID]** and **[Client Secret]** keys.
@ -71,15 +73,39 @@ Next you need to ensure that the Pomerium's Redirect URL is listed in allowed re
![Add Reply URL](./microsoft/azure-redirect-url.png)
The final, and most unique step to Azure AD provider, is to take note of your specific endpoint. Navigate to **Azure Active Directory** -> **Apps registrations** and select your app. ![Application dashboard](./microsoft/azure-application-dashbaord.png) Click on **Endpoints**
Next, in order to retrieve group information from Active Directory, we need to enable the necessary permissions for the [Microsoft Graph API](https://docs.microsoft.com/en-us/graph/auth-v2-service#azure-ad-endpoint-considerations).
On the **App registrations** page, click **API permissions**. Click the **Add a permission** button and select **Microsoft Graph API**, select **Delegated permissions**. Under the **Directory** row, select the checkbox for **Directory.Read.All**.
![Azure add group membership claims](./microsoft/azure-api-settings.png)
You can also optionally select **grant admin consent for all users** which will suppress the permission screen on first login for users.
The final, and most unique step to Azure AD provider, is to take note of your specific endpoint. Navigate to **Azure Active Directory** -> **Apps registrations** and select your app.
![Application dashboard](./microsoft/azure-application-dashbaord.png)
Click on **Endpoints**
![Endpoint details](./microsoft/azure-endpoints.png)
The **OpenID Connect Metadata Document** value will form the basis for Pomerium's **Provider URL** setting. For example, if your **Azure OpenID Connect** is `https://login.microsoftonline.com/0303f438-3c5c-4190-9854-08d3eb31bd9f/v2.0/.well-known/openid-configuration` your **Pomerium Identity Provider URL** would be `https://login.microsoftonline.com/0303f438-3c5c-4190-9854-08d3eb31bd9f/v2.0`.
The **OpenID Connect Metadata Document** value will form the basis for Pomerium's **Provider URL** setting.
For example if the **Azure OpenID Connect** url is:
```bash
https://login.microsoftonline.com/0303f438-3c5c-4190-9854-08d3eb31bd9f/v2.0/.well-known/openid-configuration`
```
**Pomerium Identity Provider URL** would be
```bash
https://login.microsoftonline.com/0303f438-3c5c-4190-9854-08d3eb31bd9f/v2.0
```
### Configure Pomerium
At this point, you will configure the integration from the Pomerium side. Your [environmental variables] should look something like:
Finally, configure Pomerium with the identity provider settings retrieved in the pervious steps. Your [environmental variables] should look something like:
```bash
# Azure
@ -94,7 +120,7 @@ IDP_CLIENT_SECRET="REPLACE-ME"
:::warning
Gitlab currently does not provide callers with a user email, under any scope, to a caller unless that user has selected her email to be public. Because Pomerium is by nature very centric, users are cautioned from using Pomerium until [this gitlab bug](https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387) is fixed.
Support was removed in v0.0.3 because Gitlab does not provide callers with a user email, under any scope, to a caller unless that user has selected her email to be public. Pomerium until [this gitlab bug](https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387) is fixed.
:::
@ -153,13 +179,52 @@ Authorized redirect URIs | [Redirect URL] (e.g.`https://auth.corp.example.com/oa
![Web App Credentials Configuration](./google/google-create-client-id-config.png)
Click **Create** to proceed.
Your [Client ID] and [Client Secret] will be displayed:
Click **Create** to proceed. The [Client ID] and [Client Secret] settings will be displayed for later configuration with Pomerium.
![OAuth Client ID and Secret](./google/google-oauth-client-info.png)
Set [Client ID] and [Client Secret] in Pomerium's settings. Your [environmental variables] should look something like this.
In order to have Pomerium validate group membership, we'll also need to configure a [service account](https://console.cloud.google.com/iam-admin/serviceaccounts) with [G-suite domain-wide delegation](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) enabled.
1. Open the [Service accounts](https://console.cloud.google.com/iam-admin/serviceaccounts) page.
2. If prompted, select a project.
3. Click **Create service** account. In the Create service account window, type a name for the service account, and select Furnish a new private key and Enable Google Apps Domain-wide Delegation.
4. Then click **Save**.
![Google create service account](./google/google-create-sa.png)
Then, you'll need to manually open an editor add an `impersonate_user` key to the downloaded public/private key file. In this case, we'd be impersonating the admin account `user@pomerium.io`.
::: warning
You MUST add the `impersonate_user` field to your json key file. [Google requires](https://stackoverflow.com/questions/48585700/is-it-possible-to-call-apis-from-service-account-without-acting-on-behalf-of-a-u/48601364#48601364) that service accounts act on behalf of another user.
:::
```json
{
"type": "service_account",
"client_id": "109818058799274859509",
...
"impersonate_user": "user@pomerium.io"
...
}
```
The base64 encoded contents of this public/private key pair json file will used for the value of the `IDP_SERVICE_ACCOUNT` configuration setting.
Next we'll delegate G-suite group membership access to the service account we just created .
1. Go to your G Suite domain's [Admin console](http://admin.google.com/).
2. Select **Security** from the list of controls. If you don't see Security listed, select More controls 1\. from the gray bar at the bottom of the page, then select Security from the list of controls.
3. Select **Advanced settings** from the list of options.
4. Select **Manage API client** access in the Authentication section.
5. In the **Client name** field enter the service account's **Client ID**.
6. In the **One or More API Scopes** field enter the following list of scopes: `https://www.googleapis.com/auth/admin.directory.group.readonly` `https://www.googleapis.com/auth/admin.directory.user.readonly`
7. Click the **Authorize** button.
![Google create service account](./google/google-gsuite-add-scopes.png)
Your [environmental variables] should look something like this.
```bash
REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback"
@ -167,6 +232,7 @@ IDP_PROVIDER="google"
IDP_PROVIDER_URL="https://accounts.google.com"
IDP_CLIENT_ID="yyyy.apps.googleusercontent.com"
IDP_CLIENT_SECRET="xxxxxx"
IDP_SERVICE_ACCOUNT="zzzz" # output of `cat service-account-key.json | base64`
```
## Okta
@ -197,7 +263,35 @@ Go to the **General** page of your app and scroll down to the **Client Credentia
![Okta Client ID and Secret](./okta/okta-client-id-and-secret.png)
At this point, you will configure the integration from the Pomerium side. Your [environmental variables] should look something like this.
Next, we'll configure Okta to pass along a custom OpenID Connect claim to establish group membership. To do so, click the **API** menu item, and select **Authorization Servers**.
![Okta authorization servers](./okta/okta-authorization-servers.png)
Select your desired authorization server and navigate to the **claims tab**. Click **Add Claim** and configure the group claim for **ID Token** as follows.
![Okta configure group claim](./okta/okta-configure-groups-claim.png)
Field | Value
--------------------- | ---------------------
Name | groups
Include in token type | **ID Token**, Always.
Value Type | Groups
Filter | Matches regex `.*`
Include in | Any scope
Add an another, almost identical, claim but this time for **Access Token**.
Field | Value
--------------------- | -------------------------
Name | groups
Include in token type | **Access Token**, Always.
Value Type | Groups
Filter | Matches regex `.*`
Include in | Any scope
![Okta list group claims](./okta/okta-list-groups-claim.png)
Finally, configure Pomerium with the identity provider settings retrieved in the pervious steps. Your [environmental variables] should look something like this.
```bash
REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback"
@ -229,13 +323,24 @@ Next, set set the **Redirect URI's** setting to be Pomerium's [redirect url].
Go to the **SSO** page. This section contains the **[Client ID]** and **[Client Secret]** you'll use to configure Pomerium.
Also, be sure to also set the application type to **Web** and the token endpoint to be **POST**.
Set the application type to **Web** and the token endpoint to be **POST**.
Under **Token Timeout settings** set **Refresh Token** to 60 minutes (or whatever value makes sense for your organization). Note, however, if you don't enable refresh tokens the user will be prompted to authenticate whenever the access token expires which can result in a poor user experience.
![One Login SSO settings](./one-login/one-login-sso-settings.png)
[OneLogin's OIDC implementation](https://developers.onelogin.com/openid-connect/scopes) supports the `groups` which can return either the user's group or role which can be used within pomerium to enforced group-based ACL policy.
At this point, you will configure the integration from the Pomerium side. Your [environmental variables] should look something like this.
To return the user's Active Directory field, configure the group to return `member_of`. In the Default if no value field, select **User Roles** and Select **Semicolon Delimited** in the adjacent field. **Select Save**
![OneLogin set role](./one-login/one-login-oidc-params.png)
**Alternatively**, groups can return the _roles_ a user is assigned. In the Default if no value field, select **User Roles** and Select **Semicolon Delimited** in the adjacent field. **Select Save**
![OneLogin set role](./one-login/one-login-oidc-groups-param.png)
Finally, configure Pomerium with the identity provider settings retrieved in the pervious steps. Your [environmental variables] should look something like this.
```bash
REDIRECT_URL="https://auth.corp.beyondperimeter.com/oauth2/callback"

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -36,24 +36,25 @@ To properly secure your app, you must use signed headers for all app types.
## Verification
To secure your app with JWT, cryptographically verify the header, payload, and signature of the JWT. The JWT is in the HTTP request header `x-pomerium-iap-jwt-assertion`. If an attacker bypasses pomerium, they can forge the unsigned identity headers, `x-pomerium-authenticated-user-{email,id}`. JWT provides a more secure alternative.
To secure your app with JWT, cryptographically verify the header, payload, and signature of the JWT. The JWT is in the HTTP request header `x-pomerium-iap-jwt-assertion`. If an attacker bypasses pomerium, they can forge the unsigned identity headers, `x-pomerium-authenticated-user-{email,id,groups}`. JWT provides a more secure alternative.
Note that pomerium it strips the `x-pomerium-*` headers provided by the client when the request goes through the serving infrastructure.
Verify that the JWT's header conforms to the following constraints:
[JWT] | description
:-----: | ---------------------------------------------------------------------------------------------------
:------: | ------------------------------------------------------------------------------------------------------
`exp` | Expiration time in seconds since the UNIX epoch. Allow 1 minute for skew.
`iat` | Issued-at time in seconds since the UNIX epoch. Allow 1 minute for skew.
`aud` | The client's final domain e.g. `httpbin.corp.example.com`.
`iss` | Issuer must be `pomerium-proxy`.
`sub` | Subject is the user's id. Can be used instead of the `x-pomerium-authenticated-user-id` header.
`email` | Email is the user's email. Can be used instead of the `x-pomerium-authenticated-user-email` header.
`groups` | Groups is the user's groups. Can be used instead of the `x-pomerium-authenticated-user-groups` header.
### Manual verification
Though you will very likely be verifying signed-headers programmatically in your application's middleware, and using a third-party JWT library, if you are new to JWT it may be helpful to show what manual verification looks like. The following guide assumes you are using the provided [docker-compose.yml] as a base and [httpbin]. Httpbin gives us a convienient way of inspecting client headers.
Though you will very likely be verifying signed-headers programmatically in your application's middleware, and using a third-party JWT library, if you are new to JWT it may be helpful to show what manual verification looks like. The following guide assumes you are using the provided [docker-compose.yml] as a base and [httpbin]. Httpbin gives us a convenient way of inspecting client headers.
1. Provide pomerium with a base64 encoded Elliptic Curve ([NIST P-256] aka [secp256r1] aka prime256v1) Private Key. In production, you'd likely want to get these from your KMS.

23
go.mod
View file

@ -1,21 +1,18 @@
module github.com/pomerium/pomerium
go 1.12
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/golang/protobuf v1.3.0
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31
github.com/pomerium/go-oidc v2.0.0+incompatible
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/rs/zerolog v1.11.0
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd
golang.org/x/oauth2 v0.0.0-20190212230446-3e8b2be13635
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a // indirect
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 // indirect
google.golang.org/genproto v0.0.0-20190215211957-bd968387e4aa // indirect
google.golang.org/grpc v1.18.0
gopkg.in/square/go-jose.v2 v2.2.2
github.com/rs/zerolog v1.12.0
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
google.golang.org/api v0.1.0
google.golang.org/grpc v1.19.0
gopkg.in/square/go-jose.v2 v2.3.0
)

80
go.sum
View file

@ -1,70 +1,82 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.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=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/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/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pomerium/envconfig v1.3.0 h1:/qJ+JOrWKkd/MgSrBDQ6xYJ7sxzqxiIAB/3qgHwdrHY=
github.com/pomerium/envconfig v1.3.0/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc=
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31 h1:bNqUesLWa+RUxQvSaV3//dEFviXdCSvMF9GKDOopFLU=
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc=
github.com/pomerium/go-oidc v2.0.0+incompatible h1:gVvG/ExWsHQqatV+uceROnGmbVYF44mDNx5nayBhC0o=
github.com/pomerium/go-oidc v2.0.0+incompatible/go.mod h1:DRsGVw6MOgxbfq4Y57jKOE8lbEfayxeiY0A8/4vxjBM=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/rs/zerolog v1.11.0 h1:DRuq/S+4k52uJzBQciUcofXx45GrMC6yrEbb/CoK6+M=
github.com/rs/zerolog v1.11.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rs/zerolog v1.12.0 h1:aqZ1XRadoS8IBknR5IDFvGzbHly1X9ApIqOroooQF/c=
github.com/rs/zerolog v1.12.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7 h1:Qe/u+eY379X4He4GBMFZYu3pmh1ML5yT1aL1ndNM1zQ=
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/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-20190212230446-3e8b2be13635 h1:dOJmQysgY8iOBECuNp0vlKHWEtfiTnyjisEizRV3/4o=
golang.org/x/oauth2 v0.0.0-20190212230446-3e8b2be13635/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
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.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190215211957-bd968387e4aa h1:FVL+/MjP2dzG4PxLpCJR7B6esIia88UAbsfYUrCc8U4=
google.golang.org/genproto v0.0.0-20190215211957-bd968387e4aa/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898 h1:yvw+zsSmSM02Z5H3ZdEV7B7Ql7eFrjQTnmByJvK+3J8=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.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.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -10,7 +10,7 @@ import (
// JWTSigner implements JWT signing according to JSON Web Token (JWT) RFC7519
// https://tools.ietf.org/html/rfc7519
type JWTSigner interface {
SignJWT(string, string) (string, error)
SignJWT(string, string, string) (string, error)
}
// ES256Signer is struct containing the required fields to create a ES256 signed JSON Web Tokens
@ -18,9 +18,15 @@ type ES256Signer struct {
// User (sub) is unique, stable identifier for the user.
// Use in place of the x-pomerium-authenticated-user-id header.
User string `json:"sub,omitempty"`
// Email (sub) is a **private** claim name identifier for the user email address.
// Email (email) is a **custom** claim name identifier for the user email address.
// Use in place of the x-pomerium-authenticated-user-email header.
Email string `json:"email,omitempty"`
// Groups (groups) is a **custom** claim name identifier for the user's groups.
// Use in place of the x-pomerium-authenticated-user-groups header.
Groups string `json:"groups,omitempty"`
// Audience (aud) must be the destination of the upstream proxy locations.
// e.g. `helloworld.corp.example.com`
Audience jwt.Audience `json:"aud,omitempty"`
@ -68,14 +74,16 @@ func NewES256Signer(privKey []byte, audience string) (*ES256Signer, error) {
}, nil
}
// SignJWT creates a signed JWT containing claims for the logged in user id (`sub`) and email (`email`).
func (s *ES256Signer) SignJWT(user, email string) (string, error) {
// SignJWT creates a signed JWT containing claims for the logged in
// user id (`sub`), email (`email`) and groups (`groups`).
func (s *ES256Signer) SignJWT(user, email, groups string) (string, error) {
s.User = user
s.Email = email
s.Groups = groups
now := time.Now()
s.IssuedAt = jwt.NewNumericDate(now)
s.Expiry = jwt.NewNumericDate(now.Add(jwt.DefaultLeeway))
s.NotBefore = jwt.NewNumericDate(now.Add(-1 * jwt.DefaultLeeway))
s.IssuedAt = *jwt.NewNumericDate(now)
s.Expiry = *jwt.NewNumericDate(now.Add(jwt.DefaultLeeway))
s.NotBefore = *jwt.NewNumericDate(now.Add(-1 * jwt.DefaultLeeway))
rawJWT, err := jwt.Signed(s.signer).Claims(s).CompactSerialize()
if err != nil {
return "", err

View file

@ -12,7 +12,7 @@ func TestES256Signer(t *testing.T) {
if signer == nil {
t.Fatal("signer should not be nil")
}
rawJwt, err := signer.SignJWT("joe-user", "joe-user@example.com")
rawJwt, err := signer.SignJWT("joe-user", "joe-user@example.com", "group1,group2")
if err != nil {
t.Fatal(err)
}

View file

@ -11,8 +11,6 @@ import (
"net/http"
"net/url"
"time"
"github.com/pomerium/pomerium/internal/log"
)
// ErrTokenRevoked signifies a token revokation or expiration error
@ -29,12 +27,12 @@ var httpClient = &http.Client{
}
// Client provides a simple helper interface to make HTTP requests
func Client(method, endpoint, userAgent string, params url.Values, response interface{}) error {
func Client(method, endpoint, userAgent string, headers map[string]string, params url.Values, response interface{}) error {
var body io.Reader
switch method {
case "POST":
case http.MethodPost:
body = bytes.NewBufferString(params.Encode())
case "GET":
case http.MethodGet:
// error checking skipped because we are just parsing in
// order to make a copy of an existing URL
u, _ := url.Parse(endpoint)
@ -49,6 +47,9 @@ func Client(method, endpoint, userAgent string, params url.Values, response inte
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", userAgent)
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := httpClient.Do(req)
if err != nil {
@ -57,12 +58,11 @@ func Client(method, endpoint, userAgent string, params url.Values, response inte
var respBody []byte
respBody, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
defer resp.Body.Close()
if err != nil {
return err
}
log.Info().Msgf("%s", respBody)
// log.Info().Msgf("%s", respBody)
if resp.StatusCode != http.StatusOK {
switch resp.StatusCode {
case http.StatusBadRequest:

273
internal/identity/google.go Normal file
View file

@ -0,0 +1,273 @@
package identity // import "github.com/pomerium/pomerium/internal/identity"
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/url"
"time"
oidc "github.com/pomerium/go-oidc"
"golang.org/x/oauth2"
"golang.org/x/oauth2/jwt"
admin "google.golang.org/api/admin/directory/v1"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version"
)
const defaultGoogleProviderURL = "https://accounts.google.com"
// JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
const JWTTokenURL = "https://accounts.google.com/o/oauth2/token"
// GoogleProvider is an implementation of the Provider interface.
type GoogleProvider struct {
*Provider
// non-standard oidc fields
RevokeURL *url.URL
apiClient *admin.Service
}
// NewGoogleProvider returns a new GoogleProvider and sets the provider url endpoints.
func NewGoogleProvider(p *Provider) (*GoogleProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
p.ProviderURL = defaultGoogleProviderURL
}
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
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: p.Scopes,
}
gp := &GoogleProvider{
Provider: p,
}
// google supports a revocation endpoint
var claims struct {
RevokeURL string `json:"revocation_endpoint"`
}
// build api client to make group membership api calls
if err := p.provider.Claims(&claims); err != nil {
return nil, err
}
// if service account set, configure admin sdk calls
if p.ServiceAccount != "" {
apiCreds, err := base64.StdEncoding.DecodeString(p.ServiceAccount)
if err != nil {
return nil, fmt.Errorf("identity/google: could not decode service account json %v", err)
}
// Required scopes for groups api
// https://developers.google.com/admin-sdk/directory/v1/reference/groups/list
conf, err := JWTConfigFromJSON(apiCreds, admin.AdminDirectoryUserReadonlyScope, admin.AdminDirectoryGroupReadonlyScope)
if err != nil {
return nil, fmt.Errorf("identity/google: failed making jwt config from json %v", err)
}
client := conf.Client(context.TODO())
gp.apiClient, err = admin.New(client)
if err != nil {
return nil, fmt.Errorf("identity/google: failed creating admin service %v", err)
}
} else {
log.Warn().Msg("identity/google: no service account found, cannot retrieve user groups")
}
gp.RevokeURL, err = url.Parse(claims.RevokeURL)
if err != nil {
return nil, err
}
return gp, nil
}
// Revoke revokes the access token a given session state.
//
// https://developers.google.com/identity/protocols/OAuth2WebServer#tokenrevoke
func (p *GoogleProvider) Revoke(accessToken string) error {
params := url.Values{}
params.Add("token", accessToken)
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), nil, params, nil)
if err != nil && err != httputil.ErrTokenRevoked {
return err
}
return nil
}
// GetSignInURL returns a URL to OAuth 2.0 provider's consent page that asks for permissions for
// the required scopes explicitly.
// Google requires an additional access scope for offline access which is a requirement for any
// application that needs to access a Google API when the user is not present.
// https://developers.google.com/identity/protocols/OAuth2WebServer#offline
func (p *GoogleProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
}
// Authenticate creates an identity session with google from a authorization code, and follows up
// call to the admin/group api to check what groups the user is in.
func (p *GoogleProvider) Authenticate(code string) (*sessions.SessionState, error) {
ctx := context.Background()
// convert authorization code into a token
oauth2Token, err := p.oauth.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("identity/google: token exchange failed %v", err)
}
// id_token contains claims about the authenticated user
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("identity/google: response did not contain an id_token")
}
// Parse and verify ID Token payload.
idToken, err := p.verifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("identity/google: could not verify id_token %v", err)
}
var claims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
// parse claims from the raw, encoded jwt token
if err := idToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("identity/google: failed to parse id_token claims %v", err)
}
// google requires additional call to retrieve groups.
groups, err := p.UserGroups(ctx, claims.Email)
if err != nil {
return nil, fmt.Errorf("identity/google: could not retrieve groups %v", err)
}
return &sessions.SessionState{
IDToken: rawIDToken,
AccessToken: oauth2Token.AccessToken,
RefreshToken: oauth2Token.RefreshToken,
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
Email: claims.Email,
User: idToken.Subject,
Groups: groups,
}, nil
}
// Refresh renews a user's session using an oid refresh token without reprompting the user.
// Group membership is also refreshed.
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
func (p *GoogleProvider) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) {
if s.RefreshToken == "" {
return nil, errors.New("identity: missing refresh token")
}
t := oauth2.Token{RefreshToken: s.RefreshToken}
newToken, err := p.oauth.TokenSource(ctx, &t).Token()
if err != nil {
log.Error().Err(err).Msg("identity: refresh failed")
return nil, err
}
s.AccessToken = newToken.AccessToken
s.RefreshDeadline = newToken.Expiry.Truncate(time.Second)
// validate groups
groups, err := p.UserGroups(ctx, s.User)
if err != nil {
return nil, fmt.Errorf("identity/google: could not retrieve groups %v", err)
}
log.Info().
Str("refresh-token", s.RefreshToken).
Str("new-access-token", newToken.AccessToken).
Str("new-expiry", time.Until(newToken.Expiry).String()).
Strs("Groups", groups).
Msg("identity: refresh")
return s, nil
}
// UserGroups returns a slice of group names a given user is in
// NOTE: groups via Directory API is limited to 1 QPS!
// https://developers.google.com/admin-sdk/directory/v1/reference/groups/list
// https://developers.google.com/admin-sdk/directory/v1/limits
func (p *GoogleProvider) UserGroups(ctx context.Context, user string) ([]string, error) {
var groups []string
if p.apiClient != nil {
req := p.apiClient.Groups.List().UserKey(user).MaxResults(100)
resp, err := req.Do()
if err != nil {
return nil, fmt.Errorf("identity/google: group api request failed %v", err)
}
for _, group := range resp.Groups {
log.Info().Str("group.Name", group.Name).Msg("sanity check3")
groups = append(groups, group.Name)
}
}
return groups, nil
}
// JWTConfigFromJSON uses a Google Developers service account JSON key file to read
// the credentials that authorize and authenticate the requests.
// Create a service account on "Credentials" for your project at
// https://console.developers.google.com to download a JSON key file.
func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
var f credentialsFile
if err := json.Unmarshal(jsonKey, &f); err != nil {
return nil, err
}
if f.Type != "service_account" {
return nil, fmt.Errorf("identity/google: 'type' field is %q (expected %q)", f.Type, "service_account")
}
// Service account must impersonate a user : https://stackoverflow.com/a/48601364
if f.ImpersonateUser == "" {
return nil, errors.New("identity/google: impersonate_user not found in json config")
}
scope = append([]string(nil), scope...) // copy
return f.jwtConfig(scope), nil
}
// credentialsFile is the unmarshalled representation of a credentials file.
type credentialsFile struct {
Type string `json:"type"` // serviceAccountKey or userCredentialsKey
// Service account must impersonate a user
ImpersonateUser string `json:"impersonate_user"`
// Service Account fields
ClientEmail string `json:"client_email"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
TokenURL string `json:"token_uri"`
ProjectID string `json:"project_id"`
// User Credential fields
ClientSecret string `json:"client_secret"`
ClientID string `json:"client_id"`
RefreshToken string `json:"refresh_token"`
}
func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config {
cfg := &jwt.Config{
Subject: f.ImpersonateUser,
Email: f.ClientEmail,
PrivateKey: []byte(f.PrivateKey),
PrivateKeyID: f.PrivateKeyID,
Scopes: scopes,
TokenURL: f.TokenURL,
}
if cfg.TokenURL == "" {
cfg.TokenURL = JWTTokenURL
}
return cfg
}

View file

@ -0,0 +1,188 @@
package identity // import "github.com/pomerium/pomerium/internal/identity"
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"time"
oidc "github.com/pomerium/go-oidc"
"golang.org/x/oauth2"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version"
)
// defaultAzureProviderURL Users with both a personal Microsoft
// account and a work or school account from Azure Active Directory (Azure AD)
// an sign in to the application.
const defaultAzureProviderURL = "https://login.microsoftonline.com/common"
const defaultAzureGroupURL = "https://graph.microsoft.com/v1.0/me/memberOf"
// AzureProvider is an implementation of the Provider interface
type AzureProvider struct {
*Provider
// non-standard oidc fields
RevokeURL *url.URL
}
// NewAzureProvider returns a new AzureProvider and sets the provider url endpoints.
// https://www.pomerium.io/docs/identity-providers.html#azure-active-directory
func NewAzureProvider(p *Provider) (*AzureProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
p.ProviderURL = defaultAzureProviderURL
}
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
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: p.Scopes,
}
azureProvider := &AzureProvider{
Provider: p,
}
// azure has a "end session endpoint"
var claims struct {
RevokeURL string `json:"end_session_endpoint"`
}
if err := p.provider.Claims(&claims); err != nil {
return nil, err
}
azureProvider.RevokeURL, err = url.Parse(claims.RevokeURL)
if err != nil {
return nil, err
}
return azureProvider, nil
}
// Authenticate creates an identity session with azure from a authorization code, and follows up
// call to the groups api to check what groups the user is in.
func (p *AzureProvider) Authenticate(code string) (*sessions.SessionState, error) {
ctx := context.Background()
// convert authorization code into a token
oauth2Token, err := p.oauth.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("identity/microsoft: token exchange failed %v", err)
}
// id_token contains claims about the authenticated user
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("identity/microsoft: response did not contain an id_token")
}
// Parse and verify ID Token payload.
idToken, err := p.verifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("identity/microsoft: could not verify id_token %v", err)
}
var claims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
// parse claims from the raw, encoded jwt token
if err := idToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("identity/microsoft: failed to parse id_token claims %v", err)
}
// google requires additional call to retrieve groups.
groups, err := p.UserGroups(ctx, claims.Email)
if err != nil {
return nil, fmt.Errorf("identity/microsoft: could not retrieve groups %v", err)
}
return &sessions.SessionState{
IDToken: rawIDToken,
AccessToken: oauth2Token.AccessToken,
RefreshToken: oauth2Token.RefreshToken,
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
Email: claims.Email,
User: idToken.Subject,
Groups: groups,
}, nil
}
// 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(token string) error {
params := url.Values{}
params.Add("token", token)
err := httputil.Client(http.MethodPost, p.RevokeURL.String(), version.UserAgent(), nil, params, nil)
if err != nil && err != httputil.ErrTokenRevoked {
return err
}
return nil
}
// GetSignInURL returns the sign in url with typical oauth parameters
func (p *AzureProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
}
// Refresh renews a user's session using an oid refresh token without reprompting the user.
// Group membership is also refreshed.
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
func (p *AzureProvider) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) {
if s.RefreshToken == "" {
return nil, errors.New("identity/microsoft: missing refresh token")
}
t := oauth2.Token{RefreshToken: s.RefreshToken}
newToken, err := p.oauth.TokenSource(ctx, &t).Token()
if err != nil {
log.Error().Err(err).Msg("identity/microsoft: refresh failed")
return nil, err
}
s.AccessToken = newToken.AccessToken
s.RefreshDeadline = newToken.Expiry.Truncate(time.Second)
s.Groups, err = p.UserGroups(ctx, s.AccessToken)
if err != nil {
log.Error().Err(err).Msg("identity/microsoft: refresh failed")
return nil, err
}
return s, nil
}
// UserGroups returns a slice of group names a given user is in.
// `Directory.Read.All` is required.
// https://docs.microsoft.com/en-us/graph/api/resources/directoryobject?view=graph-rest-1.0
// https://docs.microsoft.com/en-us/graph/api/user-list-memberof?view=graph-rest-1.0
func (p *AzureProvider) UserGroups(ctx context.Context, accessToken string) ([]string, error) {
var response struct {
Groups []struct {
ID string `json:"id"`
Description string `json:"description,omitempty"`
DisplayName string `json:"displayName"`
CreatedDateTime time.Time `json:"createdDateTime,omitempty"`
GroupTypes []string `json:"groupTypes,omitempty"`
} `json:"value"`
}
headers := map[string]string{"Authorization": fmt.Sprintf("Bearer %s", accessToken)}
err := httputil.Client(http.MethodGet, defaultAzureGroupURL, version.UserAgent(), headers, nil, &response)
if err != nil {
return nil, err
}
var groups []string
for _, group := range response.Groups {
log.Info().Str("DisplayName", group.DisplayName).Str("ID", group.ID).Msg("identity/microsoft: group")
groups = append(groups, group.DisplayName)
}
return groups, nil
}

View file

@ -1,8 +1,9 @@
package providers // import "github.com/pomerium/pomerium/internal/providers"
package identity // import "github.com/pomerium/pomerium/internal/identity"
import (
"github.com/pomerium/pomerium/internal/sessions" // type Provider interface {
"golang.org/x/oauth2"
"context"
"github.com/pomerium/pomerium/internal/sessions"
)
// MockProvider provides a mocked implementation of the providers interface.
@ -11,7 +12,7 @@ type MockProvider struct {
AuthenticateError error
ValidateResponse bool
ValidateError error
RefreshResponse *oauth2.Token
RefreshResponse *sessions.SessionState
RefreshError error
RevokeError error
GetSignInURLResponse string
@ -23,12 +24,12 @@ func (mp MockProvider) Authenticate(code string) (*sessions.SessionState, error)
}
// Validate is a mocked providers function.
func (mp MockProvider) Validate(s string) (bool, error) {
func (mp MockProvider) Validate(ctx context.Context, s string) (bool, error) {
return mp.ValidateResponse, mp.ValidateError
}
// Refresh is a mocked providers function.
func (mp MockProvider) Refresh(s string) (*oauth2.Token, error) {
func (mp MockProvider) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) {
return mp.RefreshResponse, mp.RefreshError
}

View file

@ -1,4 +1,4 @@
package providers // import "github.com/pomerium/pomerium/internal/providers"
package identity // import "github.com/pomerium/pomerium/internal/identity"
import (
"context"
@ -9,13 +9,13 @@ import (
// OIDCProvider provides a standard, OpenID Connect implementation
// of an authorization identity provider.
// see : https://openid.net/specs/openid-connect-core-1_0.html
// https://openid.net/specs/openid-connect-core-1_0.html
type OIDCProvider struct {
*IdentityProvider
*Provider
}
// NewOIDCProvider creates a new instance of an OpenID Connect provider.
func NewOIDCProvider(p *IdentityProvider) (*OIDCProvider, error) {
func NewOIDCProvider(p *Provider) (*OIDCProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
return nil, ErrMissingProviderURL
@ -36,5 +36,5 @@ func NewOIDCProvider(p *IdentityProvider) (*OIDCProvider, error) {
RedirectURL: p.RedirectURL.String(),
Scopes: p.Scopes,
}
return &OIDCProvider{IdentityProvider: p}, nil
return &OIDCProvider{Provider: p}, nil
}

138
internal/identity/okta.go Normal file
View file

@ -0,0 +1,138 @@
package identity // import "github.com/pomerium/pomerium/internal/identity"
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
oidc "github.com/pomerium/go-oidc"
"golang.org/x/oauth2"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version"
)
// OktaProvider represents the Okta Identity Provider
//
// https://www.pomerium.io/docs/identity-providers.html#okta
type OktaProvider struct {
*Provider
RevokeURL *url.URL
}
// NewOktaProvider creates a new instance of Okta as an identity provider.
func NewOktaProvider(p *Provider) (*OktaProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
return nil, ErrMissingProviderURL
}
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
if err != nil {
return nil, err
}
if len(p.Scopes) == 0 {
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "groups", "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: p.Scopes,
}
// 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{Provider: p}
oktaProvider.RevokeURL, err = url.Parse(claims.RevokeURL)
if err != nil {
return nil, err
}
return &oktaProvider, nil
}
// Revoke revokes the access token a given session state.
// https://developer.okta.com/docs/api/resources/oidc#revoke
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", token)
params.Add("token_type_hint", "refresh_token")
err := httputil.Client(http.MethodPost, p.RevokeURL.String(), version.UserAgent(), nil, params, nil)
if err != nil && err != httputil.ErrTokenRevoked {
return err
}
return nil
}
// GetSignInURL returns the sign in url with typical oauth parameters
// Google requires access type offline
func (p *OktaProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline)
}
type accessToken struct {
Subject string `json:"sub"`
Groups []string `json:"groups"`
}
// Refresh renews a user's session using an oid refresh token without reprompting the user.
// Group membership is also refreshed. If configured properly, Okta is we can configure the access token
// to include group membership claims which allows us to avoid a follow up oauth2 call.
func (p *OktaProvider) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) {
if s.RefreshToken == "" {
return nil, errors.New("identity/okta: missing refresh token")
}
t := oauth2.Token{RefreshToken: s.RefreshToken}
newToken, err := p.oauth.TokenSource(ctx, &t).Token()
if err != nil {
log.Error().Err(err).Msg("identity/okta: refresh failed")
return nil, err
}
payload, err := parseJWT(newToken.AccessToken)
if err != nil {
return nil, fmt.Errorf("identity/okta: malformed access token jwt: %v", err)
}
var token accessToken
if err := json.Unmarshal(payload, &token); err != nil {
return nil, fmt.Errorf("identity/okta: failed to unmarshal access token claims: %v", err)
}
if len(token.Groups) != 0 {
s.Groups = token.Groups
}
s.AccessToken = newToken.AccessToken
s.RefreshDeadline = newToken.Expiry.Truncate(time.Second)
return s, nil
}
func parseJWT(p string) ([]byte, error) {
parts := strings.Split(p, ".")
if len(parts) < 2 {
return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts))
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err)
}
return payload, nil
}

View file

@ -0,0 +1,142 @@
package identity // import "github.com/pomerium/pomerium/internal/identity"
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"time"
oidc "github.com/pomerium/go-oidc"
"golang.org/x/oauth2"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version"
)
// OneLoginProvider provides a standard, OpenID Connect implementation
// of an authorization identity provider.
type OneLoginProvider struct {
*Provider
// non-standard oidc fields
RevokeURL *url.URL
AdminCreds *credentialsFile
}
const defaultOneLoginProviderURL = "https://openid-connect.onelogin.com/oidc"
// NewOneLoginProvider creates a new instance of an OpenID Connect provider.
func NewOneLoginProvider(p *Provider) (*OneLoginProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
p.ProviderURL = defaultOneLoginProviderURL
}
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
if err != nil {
return nil, err
}
if len(p.Scopes) == 0 {
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "groups", "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: p.Scopes,
}
// okta supports a revocation endpoint
var claims struct {
RevokeURL string `json:"revocation_endpoint"`
}
if err := p.provider.Claims(&claims); err != nil {
return nil, err
}
OneLoginProvider := OneLoginProvider{Provider: p}
OneLoginProvider.RevokeURL, err = url.Parse(claims.RevokeURL)
if err != nil {
return nil, err
}
return &OneLoginProvider, nil
}
// Revoke revokes the access token a given session state.
// https://developers.onelogin.com/openid-connect/api/revoke-session
func (p *OneLoginProvider) Revoke(token string) error {
params := url.Values{}
params.Add("client_id", p.ClientID)
params.Add("client_secret", p.ClientSecret)
params.Add("token", token)
params.Add("token_type_hint", "access_token")
err := httputil.Client("POST", p.RevokeURL.String(), version.UserAgent(), nil, params, nil)
if err != nil && err != httputil.ErrTokenRevoked {
log.Error().Err(err).Msg("authenticate/providers: failed to revoke session")
return err
}
return nil
}
// GetSignInURL returns the sign in url with typical oauth parameters
func (p *OneLoginProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline)
}
// Refresh renews a user's session using an oid refresh token without reprompting the user.
// Group membership is also refreshed.
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
func (p *OneLoginProvider) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) {
if s.RefreshToken == "" {
return nil, errors.New("identity/microsoft: missing refresh token")
}
t := oauth2.Token{RefreshToken: s.RefreshToken}
newToken, err := p.oauth.TokenSource(ctx, &t).Token()
if err != nil {
log.Error().Err(err).Msg("identity/microsoft: refresh failed")
return nil, err
}
s.AccessToken = newToken.AccessToken
s.RefreshDeadline = newToken.Expiry.Truncate(time.Second)
s.Groups, err = p.UserGroups(ctx, s.AccessToken)
if err != nil {
log.Error().Err(err).Msg("identity/microsoft: refresh failed")
return nil, err
}
return s, nil
}
const defaultOneloginGroupURL = "https://openid-connect.onelogin.com/oidc/me"
// UserGroups returns a slice of group names a given user is in.
// https://developers.onelogin.com/openid-connect/api/user-info
func (p *OneLoginProvider) UserGroups(ctx context.Context, accessToken string) ([]string, error) {
var response struct {
User string `json:"sub"`
Email string `json:"email"`
PreferredUsername string `json:"preferred_username"`
Name string `json:"name"`
UpdatedAt time.Time `json:"updated_at"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Groups []string `json:"groups"`
}
headers := map[string]string{"Authorization": fmt.Sprintf("Bearer %s", accessToken)}
err := httputil.Client(http.MethodGet, defaultOneloginGroupURL, version.UserAgent(), headers, nil, &response)
if err != nil {
return nil, err
}
var groups []string
for _, group := range response.Groups {
log.Info().Str("ID", group).Msg("identity/onelogin: group")
groups = append(groups, group)
}
return groups, nil
}

View file

@ -1,6 +1,8 @@
//go:generate protoc -I ../../proto/authenticate --go_out=plugins=grpc:../../proto/authenticate ../../proto/authenticate/authenticate.proto
package providers // import "github.com/pomerium/pomerium/internal/providers"
// Package identity provides support for making OpenID Connect and OAuth2 authorized and
// authenticated HTTP requests with third party identity providers.
package identity // import "github.com/pomerium/pomerium/internal/identity"
import (
"context"
@ -31,49 +33,55 @@ const (
OneLoginProviderName = "onelogin"
)
var (
// ErrMissingProviderURL is returned when the CB state is half open and the requests count is over the cb maxRequests
ErrMissingProviderURL = errors.New("proxy/providers: missing provider url")
)
// ErrMissingProviderURL is returned when an identity provider requires a provider url
// does not receive one.
var ErrMissingProviderURL = errors.New("identity: missing provider url")
// Provider is an interface exposing functions necessary to interact with a given provider.
type Provider interface {
// UserGrouper is an interface representing the ability to retrieve group membership information
// from an identity provider
type UserGrouper interface {
// UserGroups returns a slice of group names a given user is in
UserGroups(context.Context, string) ([]string, error)
}
// Authenticator is an interface representing the ability to authenticate with an identity provider.
type Authenticator interface {
Authenticate(string) (*sessions.SessionState, error)
Validate(string) (bool, error)
Refresh(string) (*oauth2.Token, error)
Validate(context.Context, string) (bool, error)
Refresh(context.Context, *sessions.SessionState) (*sessions.SessionState, error)
Revoke(string) error
GetSignInURL(state string) string
}
// New returns a new identity provider based given its name.
// 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) {
func New(providerName string, p *Provider) (a Authenticator, err error) {
switch providerName {
case AzureProviderName:
p, err = NewAzureProvider(pd)
a, err = NewAzureProvider(p)
case GitlabProviderName:
p, err = NewGitlabProvider(pd)
return nil, fmt.Errorf("identity: %s currently not supported", providerName)
case GoogleProviderName:
p, err = NewGoogleProvider(pd)
a, err = NewGoogleProvider(p)
case OIDCProviderName:
p, err = NewOIDCProvider(pd)
a, err = NewOIDCProvider(p)
case OktaProviderName:
p, err = NewOktaProvider(pd)
a, err = NewOktaProvider(p)
case OneLoginProviderName:
p, err = NewOneLoginProvider(pd)
a, err = NewOneLoginProvider(p)
default:
return nil, fmt.Errorf("authenticate: %q name not found", providerName)
return nil, fmt.Errorf("identity: %s provider not known", providerName)
}
if err != nil {
return nil, err
}
return p, nil
return a, nil
}
// IdentityProvider contains the fields required for an OAuth 2.0 Authorization Request that
// Provider 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 {
type Provider struct {
ProviderName string
RedirectURL *url.URL
@ -83,6 +91,10 @@ type IdentityProvider struct {
Scopes []string
SessionLifetimeTTL time.Duration
// Some providers, such as google, require additional remote api calls to retrieve
// user details like groups. Provider is responsible for parsing.
ServiceAccount string
provider *oidc.Provider
verifier *oidc.IDTokenVerifier
oauth *oauth2.Config
@ -95,8 +107,8 @@ type IdentityProvider struct {
// 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)
func (p *Provider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline)
}
// Validate validates a given session's from it's JWT token
@ -106,40 +118,32 @@ func (p *IdentityProvider) GetSignInURL(state string) string {
// 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()
func (p *Provider) Validate(ctx context.Context, idToken string) (bool, error) {
_, err := p.verifier.Verify(ctx, idToken)
if err != nil {
log.Error().Err(err).Msg("authenticate/providers: failed to verify session state")
log.Error().Err(err).Msg("identity: failed to verify session state")
return false, err
}
return true, nil
}
// Authenticate creates a session with an identity provider from a authorization code
func (p *IdentityProvider) Authenticate(code string) (*sessions.SessionState, error) {
func (p *Provider) Authenticate(code string) (*sessions.SessionState, error) {
ctx := context.Background()
// convert authorization code into a token
oauth2Token, err := p.oauth.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("authenticate/providers: failed token exchange: %v", err)
return nil, fmt.Errorf("identity: failed token exchange: %v", err)
}
log.Info().
Str("RefreshToken", oauth2Token.RefreshToken).
Str("TokenType", oauth2Token.TokenType).
Str("AccessToken", oauth2Token.AccessToken).
Msg("Authenticate - oauth.Exchange")
//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")
}
// Parse and verify ID Token payload.
idToken, err := p.verifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("authenticate/providers: could not verify id_token: %v", err)
return nil, fmt.Errorf("identity: could not verify id_token: %v", err)
}
// Extract id_token which contains claims about the authenticated user
@ -150,14 +154,14 @@ func (p *IdentityProvider) Authenticate(code string) (*sessions.SessionState, er
}
// 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)
return nil, fmt.Errorf("identity: failed to parse id_token claims: %v", err)
}
return &sessions.SessionState{
IDToken: rawIDToken,
AccessToken: oauth2Token.AccessToken,
RefreshToken: oauth2Token.RefreshToken,
RefreshDeadline: oauth2Token.Expiry,
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
Email: claims.Email,
User: idToken.Subject,
@ -165,28 +169,26 @@ func (p *IdentityProvider) Authenticate(code string) (*sessions.SessionState, er
}, nil
}
// 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 nil, errors.New("authenticate/providers: missing refresh token")
// Refresh renews a user's session using an oid refresh token without reprompting the user.
// Group membership is also refreshed.
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
func (p *Provider) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) {
if s.RefreshToken == "" {
return nil, errors.New("identity: missing refresh token")
}
t := oauth2.Token{RefreshToken: refreshToken}
newToken, err := p.oauth.TokenSource(context.Background(), &t).Token()
t := oauth2.Token{RefreshToken: s.RefreshToken}
newToken, err := p.oauth.TokenSource(ctx, &t).Token()
if err != nil {
log.Error().Err(err).Msg("authenticate/providers.Refresh")
log.Error().Err(err).Msg("identity: refresh failed")
return nil, err
}
log.Info().
Str("RefreshToken", refreshToken).
Str("newToken.AccessToken", newToken.AccessToken).
Str("time.Until(newToken.Expiry)", time.Until(newToken.Expiry).String()).
Msg("authenticate/providers.Refresh")
return newToken, nil
s.AccessToken = newToken.AccessToken
s.RefreshDeadline = newToken.Expiry.Truncate(time.Second)
return s, 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 *IdentityProvider) Revoke(token string) error {
return errors.New("authenticate/providers: revoke not implemented")
func (p *Provider) Revoke(token string) error {
return fmt.Errorf("identity: revoke not implemented by %s", p.ProviderName)
}

View file

@ -15,7 +15,7 @@ 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)
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}
// With creates a child logger with the field added to its context.

View file

@ -35,7 +35,7 @@ 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}
return fileDescriptor_authenticate_2c495f1e6e8d5900, []int{0}
}
func (m *AuthenticateRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_AuthenticateRequest.Unmarshal(m, b)
@ -62,84 +62,6 @@ func (m *AuthenticateRequest) GetCode() string {
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:"-"`
@ -151,7 +73,7 @@ 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}
return fileDescriptor_authenticate_2c495f1e6e8d5900, []int{1}
}
func (m *ValidateRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ValidateRequest.Unmarshal(m, b)
@ -189,7 +111,7 @@ 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}
return fileDescriptor_authenticate_2c495f1e6e8d5900, []int{2}
}
func (m *ValidateReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ValidateReply.Unmarshal(m, b)
@ -216,97 +138,105 @@ func (m *ValidateReply) GetIsValid() bool {
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 {
type Session 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"`
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"`
Groups []string `protobuf:"bytes,6,rep,name=groups,proto3" json:"groups,omitempty"`
RefreshDeadline *timestamp.Timestamp `protobuf:"bytes,7,opt,name=refresh_deadline,json=refreshDeadline,proto3" json:"refresh_deadline,omitempty"`
LifetimeDeadline *timestamp.Timestamp `protobuf:"bytes,8,opt,name=lifetime_deadline,json=lifetimeDeadline,proto3" json:"lifetime_deadline,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 *Session) Reset() { *m = Session{} }
func (m *Session) String() string { return proto.CompactTextString(m) }
func (*Session) ProtoMessage() {}
func (*Session) Descriptor() ([]byte, []int) {
return fileDescriptor_authenticate_2c495f1e6e8d5900, []int{3}
}
func (m *RefreshReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_RefreshReply.Unmarshal(m, b)
func (m *Session) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Session.Unmarshal(m, b)
}
func (m *RefreshReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_RefreshReply.Marshal(b, m, deterministic)
func (m *Session) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Session.Marshal(b, m, deterministic)
}
func (dst *RefreshReply) XXX_Merge(src proto.Message) {
xxx_messageInfo_RefreshReply.Merge(dst, src)
func (dst *Session) XXX_Merge(src proto.Message) {
xxx_messageInfo_Session.Merge(dst, src)
}
func (m *RefreshReply) XXX_Size() int {
return xxx_messageInfo_RefreshReply.Size(m)
func (m *Session) XXX_Size() int {
return xxx_messageInfo_Session.Size(m)
}
func (m *RefreshReply) XXX_DiscardUnknown() {
xxx_messageInfo_RefreshReply.DiscardUnknown(m)
func (m *Session) XXX_DiscardUnknown() {
xxx_messageInfo_Session.DiscardUnknown(m)
}
var xxx_messageInfo_RefreshReply proto.InternalMessageInfo
var xxx_messageInfo_Session proto.InternalMessageInfo
func (m *RefreshReply) GetAccessToken() string {
func (m *Session) GetAccessToken() string {
if m != nil {
return m.AccessToken
}
return ""
}
func (m *RefreshReply) GetExpiry() *timestamp.Timestamp {
func (m *Session) GetRefreshToken() string {
if m != nil {
return m.Expiry
return m.RefreshToken
}
return ""
}
func (m *Session) GetIdToken() string {
if m != nil {
return m.IdToken
}
return ""
}
func (m *Session) GetUser() string {
if m != nil {
return m.User
}
return ""
}
func (m *Session) GetEmail() string {
if m != nil {
return m.Email
}
return ""
}
func (m *Session) GetGroups() []string {
if m != nil {
return m.Groups
}
return nil
}
func (m *Session) GetRefreshDeadline() *timestamp.Timestamp {
if m != nil {
return m.RefreshDeadline
}
return nil
}
func (m *Session) GetLifetimeDeadline() *timestamp.Timestamp {
if m != nil {
return m.LifetimeDeadline
}
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")
proto.RegisterType((*Session)(nil), "authenticate.Session")
}
// Reference imports to suppress errors if they are not otherwise used.
@ -321,9 +251,9 @@ const _ = grpc.SupportPackageIsVersion4
//
// 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)
Authenticate(ctx context.Context, in *AuthenticateRequest, opts ...grpc.CallOption) (*Session, error)
Validate(ctx context.Context, in *ValidateRequest, opts ...grpc.CallOption) (*ValidateReply, error)
Refresh(ctx context.Context, in *RefreshRequest, opts ...grpc.CallOption) (*RefreshReply, error)
Refresh(ctx context.Context, in *Session, opts ...grpc.CallOption) (*Session, error)
}
type authenticatorClient struct {
@ -334,8 +264,8 @@ 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)
func (c *authenticatorClient) Authenticate(ctx context.Context, in *AuthenticateRequest, opts ...grpc.CallOption) (*Session, error) {
out := new(Session)
err := c.cc.Invoke(ctx, "/authenticate.Authenticator/Authenticate", in, out, opts...)
if err != nil {
return nil, err
@ -352,8 +282,8 @@ func (c *authenticatorClient) Validate(ctx context.Context, in *ValidateRequest,
return out, nil
}
func (c *authenticatorClient) Refresh(ctx context.Context, in *RefreshRequest, opts ...grpc.CallOption) (*RefreshReply, error) {
out := new(RefreshReply)
func (c *authenticatorClient) Refresh(ctx context.Context, in *Session, opts ...grpc.CallOption) (*Session, error) {
out := new(Session)
err := c.cc.Invoke(ctx, "/authenticate.Authenticator/Refresh", in, out, opts...)
if err != nil {
return nil, err
@ -363,9 +293,9 @@ func (c *authenticatorClient) Refresh(ctx context.Context, in *RefreshRequest, o
// AuthenticatorServer is the server API for Authenticator service.
type AuthenticatorServer interface {
Authenticate(context.Context, *AuthenticateRequest) (*AuthenticateReply, error)
Authenticate(context.Context, *AuthenticateRequest) (*Session, error)
Validate(context.Context, *ValidateRequest) (*ValidateReply, error)
Refresh(context.Context, *RefreshRequest) (*RefreshReply, error)
Refresh(context.Context, *Session) (*Session, error)
}
func RegisterAuthenticatorServer(s *grpc.Server, srv AuthenticatorServer) {
@ -409,7 +339,7 @@ func _Authenticator_Validate_Handler(srv interface{}, ctx context.Context, dec f
}
func _Authenticator_Refresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RefreshRequest)
in := new(Session)
if err := dec(in); err != nil {
return nil, err
}
@ -421,7 +351,7 @@ func _Authenticator_Refresh_Handler(srv interface{}, ctx context.Context, dec fu
FullMethod: "/authenticate.Authenticator/Refresh",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthenticatorServer).Refresh(ctx, req.(*RefreshRequest))
return srv.(AuthenticatorServer).Refresh(ctx, req.(*Session))
}
return interceptor(ctx, in, info, handler)
}
@ -447,31 +377,32 @@ var _Authenticator_serviceDesc = grpc.ServiceDesc{
Metadata: "authenticate.proto",
}
func init() { proto.RegisterFile("authenticate.proto", fileDescriptor_authenticate_b52fdd447b0a5778) }
func init() { proto.RegisterFile("authenticate.proto", fileDescriptor_authenticate_2c495f1e6e8d5900) }
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,
var fileDescriptor_authenticate_2c495f1e6e8d5900 = []byte{
// 378 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x92, 0xcf, 0x6e, 0x9b, 0x40,
0x10, 0xc6, 0x8d, 0xff, 0x81, 0xc7, 0x58, 0x76, 0xa7, 0x7f, 0x44, 0xa9, 0xaa, 0xda, 0xf4, 0xe2,
0x56, 0x15, 0x96, 0xdc, 0x53, 0x8f, 0x95, 0x5a, 0x25, 0xca, 0x91, 0x58, 0xb9, 0x5a, 0x18, 0xc6,
0xf6, 0x2a, 0x98, 0x25, 0xec, 0x12, 0xc9, 0x2f, 0x97, 0x67, 0xc9, 0xa3, 0x44, 0x2c, 0x20, 0x43,
0x64, 0x2b, 0x37, 0x66, 0xf6, 0x37, 0x1f, 0xb3, 0xdf, 0xb7, 0x80, 0x7e, 0x26, 0xf7, 0x14, 0x4b,
0x16, 0xf8, 0x92, 0xdc, 0x24, 0xe5, 0x92, 0xa3, 0x59, 0xef, 0xd9, 0xdf, 0x76, 0x9c, 0xef, 0x22,
0x5a, 0xa8, 0xb3, 0x4d, 0xb6, 0x5d, 0x48, 0x76, 0x20, 0x21, 0xfd, 0x43, 0x52, 0xe0, 0xce, 0x0f,
0x78, 0xff, 0xb7, 0x36, 0xe0, 0xd1, 0x43, 0x46, 0x42, 0x22, 0x42, 0x37, 0xe0, 0x21, 0x59, 0xda,
0x54, 0x9b, 0x0f, 0x3c, 0xf5, 0xed, 0xfc, 0x82, 0xf1, 0x9d, 0x1f, 0xb1, 0xb0, 0x86, 0x7d, 0x06,
0x83, 0x85, 0x6b, 0xc9, 0xef, 0x29, 0x2e, 0x51, 0x9d, 0x85, 0xab, 0xbc, 0x74, 0x7e, 0xc2, 0xe8,
0x44, 0x27, 0xd1, 0x51, 0xb1, 0x62, 0xfd, 0x98, 0xf7, 0x14, 0x6b, 0x78, 0x3a, 0x13, 0x0a, 0x71,
0x9e, 0xda, 0xa0, 0xdf, 0x92, 0x10, 0x8c, 0xc7, 0x38, 0x03, 0xd3, 0x0f, 0x02, 0x12, 0xa2, 0x21,
0x3b, 0x2c, 0x7a, 0x4a, 0x1a, 0xbf, 0xc3, 0x28, 0xa5, 0x6d, 0x4a, 0x62, 0x5f, 0x32, 0x6d, 0xc5,
0x98, 0x65, 0xb3, 0x80, 0xea, 0xab, 0x75, 0x1a, 0xab, 0xe5, 0x97, 0xcb, 0x04, 0xa5, 0x56, 0xb7,
0xb8, 0x5c, 0xfe, 0x8d, 0x1f, 0xa0, 0x47, 0x07, 0x9f, 0x45, 0x56, 0x4f, 0x35, 0x8b, 0x02, 0x3f,
0x41, 0x7f, 0x97, 0xf2, 0x2c, 0x11, 0x56, 0x7f, 0xda, 0x99, 0x0f, 0xbc, 0xb2, 0xc2, 0xff, 0x30,
0xa9, 0x36, 0x08, 0xc9, 0x0f, 0x23, 0x16, 0x93, 0xa5, 0x4f, 0xb5, 0xf9, 0x70, 0x69, 0xbb, 0x85,
0xe3, 0x6e, 0xe5, 0xb8, 0xbb, 0xaa, 0x1c, 0xf7, 0xc6, 0xe5, 0xcc, 0xbf, 0x72, 0x04, 0xaf, 0xe0,
0x5d, 0xc4, 0xb6, 0x94, 0x67, 0x72, 0xd2, 0x31, 0xde, 0xd4, 0x99, 0x54, 0x43, 0x95, 0xd0, 0xf2,
0x59, 0x83, 0x51, 0x2d, 0x46, 0x9e, 0xe2, 0x0d, 0x98, 0xf5, 0x5c, 0x71, 0xe6, 0x36, 0xde, 0xca,
0x99, 0xcc, 0xed, 0x8f, 0x4d, 0xa4, 0x0c, 0xc4, 0x69, 0xe1, 0x35, 0x18, 0x55, 0x94, 0xf8, 0xb5,
0x09, 0xbd, 0x7a, 0x10, 0xf6, 0x97, 0x4b, 0xc7, 0x49, 0x74, 0x74, 0x5a, 0xf8, 0x07, 0x74, 0xaf,
0xf0, 0x00, 0xcf, 0xff, 0xed, 0xe2, 0x12, 0x9b, 0xbe, 0x32, 0xe2, 0xf7, 0x4b, 0x00, 0x00, 0x00,
0xff, 0xff, 0xcb, 0xcf, 0xe8, 0x63, 0xf4, 0x02, 0x00, 0x00,
}

View file

@ -4,29 +4,24 @@ import "google/protobuf/timestamp.proto";
package authenticate;
service Authenticator {
rpc Authenticate(AuthenticateRequest) returns (AuthenticateReply) {}
rpc Authenticate(AuthenticateRequest) returns (Session) {}
rpc Validate(ValidateRequest) returns (ValidateReply) {}
rpc Refresh(RefreshRequest) returns (RefreshReply) {}
rpc Refresh(Session) returns (Session) {}
}
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 {
message Session {
string access_token = 1;
google.protobuf.Timestamp expiry = 2;
string refresh_token = 2;
string id_token = 3;
string user = 4;
string email = 5;
repeated string groups = 6;
google.protobuf.Timestamp refresh_deadline = 7;
google.protobuf.Timestamp lifetime_deadline = 8;
}

View file

@ -0,0 +1,58 @@
package authenticate
import (
fmt "fmt"
"github.com/golang/protobuf/ptypes"
"github.com/pomerium/pomerium/internal/sessions"
)
// SessionFromProto converts a converts a protocol buffer session into a pomerium session state.
func SessionFromProto(p *Session) (*sessions.SessionState, error) {
if p == nil {
return nil, fmt.Errorf("proto/authenticate: SessionFromProto session cannot be nil")
}
lifetimeDeadline, err := ptypes.Timestamp(p.LifetimeDeadline)
if err != nil {
return nil, fmt.Errorf("proto/authenticate: couldn't parse lifetime deadline %v", err)
}
refreshDeadline, err := ptypes.Timestamp(p.RefreshDeadline)
if err != nil {
return nil, fmt.Errorf("proto/authenticate: couldn't parse refresh deadline %v", err)
}
return &sessions.SessionState{
AccessToken: p.AccessToken,
RefreshToken: p.RefreshToken,
IDToken: p.IdToken,
Email: p.Email,
User: p.User,
Groups: p.Groups,
RefreshDeadline: refreshDeadline,
LifetimeDeadline: lifetimeDeadline,
}, nil
}
// ProtoFromSession converts a pomerium user session into a protocol buffer struct.
func ProtoFromSession(s *sessions.SessionState) (*Session, error) {
if s == nil {
return nil, fmt.Errorf("proto/authenticate: ProtoFromSession session cannot be nil")
}
lifetimeDeadline, err := ptypes.TimestampProto(s.LifetimeDeadline)
if err != nil {
return nil, fmt.Errorf("proto/authenticate: couldn't parse lifetime deadline %v", err)
}
refreshDeadline, err := ptypes.TimestampProto(s.RefreshDeadline)
if err != nil {
return nil, fmt.Errorf("proto/authenticate: couldn't parse refresh deadline %v", err)
}
return &Session{
AccessToken: s.AccessToken,
RefreshToken: s.RefreshToken,
IdToken: s.IDToken,
Email: s.Email,
User: s.User,
Groups: s.Groups,
RefreshDeadline: refreshDeadline,
LifetimeDeadline: lifetimeDeadline,
}, nil
}

View file

@ -60,19 +60,19 @@ func TestAuthenticate(t *testing.T) {
mockAuthenticateClient := mock.NewMockAuthenticatorClient(ctrl)
mockExpire, err := ptypes.TimestampProto(fixedDate)
if err != nil {
t.Fatalf("%v failed converting timestampe", err)
t.Fatalf("%v failed converting timestamp", err)
}
req := &pb.AuthenticateRequest{Code: "unit_test"}
mockAuthenticateClient.EXPECT().Authenticate(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.AuthenticateReply{
).Return(&pb.Session{
AccessToken: "mocked access token",
RefreshToken: "mocked refresh token",
IdToken: "mocked id token",
User: "user1",
Email: "test@email.com",
Expiry: mockExpire,
LifetimeDeadline: mockExpire,
}, nil)
testAuthenticate(t, mockAuthenticateClient)
}
@ -107,15 +107,15 @@ func TestRefresh(t *testing.T) {
mockRefreshClient := mock.NewMockAuthenticatorClient(ctrl)
mockExpire, err := ptypes.TimestampProto(fixedDate)
if err != nil {
t.Fatalf("%v failed converting timestampe", err)
t.Fatalf("%v failed converting timestamp", err)
}
req := &pb.RefreshRequest{RefreshToken: "unit_test"}
req := &pb.Session{RefreshToken: "unit_test"}
mockRefreshClient.EXPECT().Refresh(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.RefreshReply{
).Return(&pb.Session{
AccessToken: "mocked access token",
Expiry: mockExpire,
LifetimeDeadline: mockExpire,
}, nil)
testRefresh(t, mockRefreshClient)
}
@ -123,16 +123,16 @@ func TestRefresh(t *testing.T) {
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"})
r, err := client.Refresh(ctx, &pb.Session{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)
respExpire, err := ptypes.Timestamp(r.LifetimeDeadline)
if err != nil {
t.Fatalf("%v failed converting timestampe", err)
t.Fatalf("%v failed converting timestamp", err)
}
if respExpire != fixedDate {

View file

@ -1,15 +1,15 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/pomerium/pomerium/proto/authenticate (interfaces: AuthenticatorClient)
// Source: proto/authenticate/authenticate.pb.go
// Package mock_authenticate is a generated GoMock package.
package mock_authenticate
import (
context "context"
"context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
authenticate "github.com/pomerium/pomerium/proto/authenticate"
"github.com/pomerium/pomerium/proto/authenticate"
grpc "google.golang.org/grpc"
)
@ -37,50 +37,30 @@ func (m *MockAuthenticatorClient) EXPECT() *MockAuthenticatorClientMockRecorder
}
// Authenticate mocks base method
func (m *MockAuthenticatorClient) Authenticate(arg0 context.Context, arg1 *authenticate.AuthenticateRequest, arg2 ...grpc.CallOption) (*authenticate.AuthenticateReply, error) {
func (m *MockAuthenticatorClient) Authenticate(ctx context.Context, in *authenticate.AuthenticateRequest, opts ...grpc.CallOption) (*authenticate.Session, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs := []interface{}{ctx, in}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Authenticate", varargs...)
ret0, _ := ret[0].(*authenticate.AuthenticateReply)
ret0, _ := ret[0].(*authenticate.Session)
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 {
func (mr *MockAuthenticatorClientMockRecorder) Authenticate(ctx, in interface{}, opts ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
varargs := append([]interface{}{ctx, in}, opts...)
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) {
func (m *MockAuthenticatorClient) Validate(ctx context.Context, in *authenticate.ValidateRequest, opts ...grpc.CallOption) (*authenticate.ValidateReply, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs := []interface{}{ctx, in}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Validate", varargs...)
@ -90,8 +70,96 @@ func (m *MockAuthenticatorClient) Validate(arg0 context.Context, arg1 *authentic
}
// Validate indicates an expected call of Validate
func (mr *MockAuthenticatorClientMockRecorder) Validate(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
func (mr *MockAuthenticatorClientMockRecorder) Validate(ctx, in interface{}, opts ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
varargs := append([]interface{}{ctx, in}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockAuthenticatorClient)(nil).Validate), varargs...)
}
// Refresh mocks base method
func (m *MockAuthenticatorClient) Refresh(ctx context.Context, in *authenticate.Session, opts ...grpc.CallOption) (*authenticate.Session, error) {
m.ctrl.T.Helper()
varargs := []interface{}{ctx, in}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Refresh", varargs...)
ret0, _ := ret[0].(*authenticate.Session)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh
func (mr *MockAuthenticatorClientMockRecorder) Refresh(ctx, in interface{}, opts ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx, in}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockAuthenticatorClient)(nil).Refresh), varargs...)
}
// MockAuthenticatorServer is a mock of AuthenticatorServer interface
type MockAuthenticatorServer struct {
ctrl *gomock.Controller
recorder *MockAuthenticatorServerMockRecorder
}
// MockAuthenticatorServerMockRecorder is the mock recorder for MockAuthenticatorServer
type MockAuthenticatorServerMockRecorder struct {
mock *MockAuthenticatorServer
}
// NewMockAuthenticatorServer creates a new mock instance
func NewMockAuthenticatorServer(ctrl *gomock.Controller) *MockAuthenticatorServer {
mock := &MockAuthenticatorServer{ctrl: ctrl}
mock.recorder = &MockAuthenticatorServerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockAuthenticatorServer) EXPECT() *MockAuthenticatorServerMockRecorder {
return m.recorder
}
// Authenticate mocks base method
func (m *MockAuthenticatorServer) Authenticate(arg0 context.Context, arg1 *authenticate.AuthenticateRequest) (*authenticate.Session, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Authenticate", arg0, arg1)
ret0, _ := ret[0].(*authenticate.Session)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Authenticate indicates an expected call of Authenticate
func (mr *MockAuthenticatorServerMockRecorder) Authenticate(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockAuthenticatorServer)(nil).Authenticate), arg0, arg1)
}
// Validate mocks base method
func (m *MockAuthenticatorServer) Validate(arg0 context.Context, arg1 *authenticate.ValidateRequest) (*authenticate.ValidateReply, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Validate", arg0, arg1)
ret0, _ := ret[0].(*authenticate.ValidateReply)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Validate indicates an expected call of Validate
func (mr *MockAuthenticatorServerMockRecorder) Validate(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockAuthenticatorServer)(nil).Validate), arg0, arg1)
}
// Refresh mocks base method
func (m *MockAuthenticatorServer) Refresh(arg0 context.Context, arg1 *authenticate.Session) (*authenticate.Session, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
ret0, _ := ret[0].(*authenticate.Session)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh
func (mr *MockAuthenticatorServerMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockAuthenticatorServer)(nil).Refresh), arg0, arg1)
}

View file

@ -1,18 +1,19 @@
package authenticator // import "github.com/pomerium/pomerium/proxy/authenticator"
import (
"time"
"context"
"github.com/pomerium/pomerium/internal/sessions"
)
// Authenticator provides the authenticate service interface
type Authenticator interface {
// Redeem takes a code and returns a validated session or an error
Redeem(string) (*RedeemResponse, error)
// Refresh attempts to refresh a valid session with a refresh token. Returns a new access token
// and expiration, or an error.
Refresh(string) (string, time.Time, error)
Redeem(context.Context, string) (*sessions.SessionState, error)
// Refresh attempts to refresh a valid session with a refresh token. Returns a refreshed session.
Refresh(context.Context, *sessions.SessionState) (*sessions.SessionState, error)
// Validate evaluates a given oidc id_token for validity. Returns validity and any error.
Validate(string) (bool, error)
Validate(context.Context, string) (bool, error)
// Close closes the authenticator connection if any.
Close() error
}

View file

@ -1,49 +1,52 @@
package authenticator
import (
"context"
"errors"
"reflect"
"testing"
"time"
"github.com/pomerium/pomerium/internal/sessions"
)
func TestMockAuthenticate(t *testing.T) {
// Absurd, but I caught a typo this way.
fixedDate := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
redeemResponse := &RedeemResponse{
redeemResponse := &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
Expiry: fixedDate,
LifetimeDeadline: fixedDate,
}
ma := &MockAuthenticate{
RedeemError: errors.New("RedeemError"),
RedeemResponse: redeemResponse,
RefreshResponse: "RefreshResponse",
RefreshTime: fixedDate,
RefreshResponse: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: fixedDate,
},
RefreshError: errors.New("RefreshError"),
ValidateResponse: true,
ValidateError: errors.New("ValidateError"),
CloseError: errors.New("CloseError"),
}
got, gotErr := ma.Redeem("a")
got, gotErr := ma.Redeem(context.Background(), "a")
if gotErr.Error() != "RedeemError" {
t.Errorf("unexpected value for gotErr %s", gotErr)
}
if !reflect.DeepEqual(redeemResponse, got) {
t.Errorf("unexpected value for redeemResponse %s", got)
}
gotToken, gotTime, gotErr := ma.Refresh("a")
newSession, gotErr := ma.Refresh(context.Background(), nil)
if gotErr.Error() != "RefreshError" {
t.Errorf("unexpected value for gotErr %s", gotErr)
}
if !reflect.DeepEqual(gotToken, "RefreshResponse") {
t.Errorf("unexpected value for gotToken %s", gotToken)
}
if !gotTime.Equal(fixedDate) {
t.Errorf("unexpected value for gotTime %s", gotTime)
if !reflect.DeepEqual(newSession, redeemResponse) {
t.Errorf("unexpected value for newSession %s", newSession)
}
ok, gotErr := ma.Validate("a")
ok, gotErr := ma.Validate(context.Background(), "a")
if !ok {
t.Errorf("unexpected value for ok : %t", ok)
}

View file

@ -10,12 +10,12 @@ import (
"strings"
"time"
"github.com/golang/protobuf/ptypes"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/sessions"
pb "github.com/pomerium/pomerium/proto/authenticate"
)
@ -92,16 +92,6 @@ func NewGRPC(opts *Options) (p *AuthenticateGRPC, err error) {
return &AuthenticateGRPC{Conn: conn, client: authClient}, nil
}
// RedeemResponse contains data from a authenticator redeem request.
type RedeemResponse struct {
AccessToken string
RefreshToken string
IDToken string
User string
Email string
Expiry time.Time
}
// AuthenticateGRPC is a gRPC implementation of an authenticator (authenticate client)
type AuthenticateGRPC struct {
Conn *grpc.ClientConn
@ -110,60 +100,73 @@ type AuthenticateGRPC struct {
// Redeem 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 (a *AuthenticateGRPC) Redeem(code string) (*RedeemResponse, error) {
func (a *AuthenticateGRPC) Redeem(ctx context.Context, code string) (*sessions.SessionState, error) {
if code == "" {
return nil, errors.New("missing code")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
r, err := a.client.Authenticate(ctx, &pb.AuthenticateRequest{Code: code})
protoSession, err := a.client.Authenticate(ctx, &pb.AuthenticateRequest{Code: code})
if err != nil {
return nil, err
}
expiry, err := ptypes.Timestamp(r.Expiry)
session, err := pb.SessionFromProto(protoSession)
if err != nil {
return nil, err
}
return &RedeemResponse{
AccessToken: r.AccessToken,
RefreshToken: r.RefreshToken,
IDToken: r.IdToken,
User: r.User,
Email: r.Email,
Expiry: expiry,
}, nil
return session, nil
}
// Refresh 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 (a *AuthenticateGRPC) Refresh(refreshToken string) (string, time.Time, error) {
if refreshToken == "" {
return "", time.Time{}, errors.New("missing refresh token")
func (a *AuthenticateGRPC) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) {
if s.RefreshToken == "" {
return nil, errors.New("missing refresh token")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
req, err := pb.ProtoFromSession(s)
if err != nil {
return nil, err
}
// todo(bdd): handle request id in grpc receiver and add to ctx logger
// reqID, ok := middleware.IDFromCtx(ctx)
// if ok {
// md := metadata.Pairs("req_id", reqID)
// ctx = metadata.NewOutgoingContext(ctx, md)
// }
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
r, err := a.client.Refresh(ctx, &pb.RefreshRequest{RefreshToken: refreshToken})
// todo(bdd): add grpc specific timeouts to main options
// todo(bdd): handle request id (metadata!?) in grpc receiver and add to ctx logger
reply, err := a.client.Refresh(ctx, req)
if err != nil {
return "", time.Time{}, err
return nil, err
}
expiry, err := ptypes.Timestamp(r.Expiry)
newSession, err := pb.SessionFromProto(reply)
if err != nil {
return "", time.Time{}, err
return nil, err
}
return r.AccessToken, expiry, nil
return newSession, nil
}
// Validate 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 (a *AuthenticateGRPC) Validate(idToken string) (bool, error) {
func (a *AuthenticateGRPC) Validate(ctx context.Context, idToken string) (bool, error) {
if idToken == "" {
return false, errors.New("missing id token")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
// todo(bdd): add grpc specific timeouts to main options
// todo(bdd): handle request id in grpc receiver and add to ctx logger
// reqID, ok := middleware.IDFromCtx(ctx)
// if ok {
// md := metadata.Pairs("req_id", reqID)
// ctx = metadata.NewOutgoingContext(ctx, md)
// }
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// todo(bdd): add grpc specific timeouts to main options
// todo(bdd): handle request id (metadata!?) in grpc receiver and add to ctx logger
r, err := a.client.Validate(ctx, &pb.ValidateRequest{IdToken: idToken})
if err != nil {
return false, err

View file

@ -1,6 +1,7 @@
package authenticator
import (
"context"
"fmt"
"reflect"
"strings"
@ -10,6 +11,7 @@ import (
"github.com/golang/mock/gomock"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
"github.com/pomerium/pomerium/internal/sessions"
pb "github.com/pomerium/pomerium/proto/authenticate"
mock "github.com/pomerium/pomerium/proto/authenticate/mock_authenticate"
)
@ -46,34 +48,36 @@ func TestProxy_Redeem(t *testing.T) {
mockAuthenticateClient.EXPECT().Authenticate(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.AuthenticateReply{
).Return(&pb.Session{
AccessToken: "mocked access token",
RefreshToken: "mocked refresh token",
IdToken: "mocked id token",
User: "user1",
Email: "test@email.com",
Expiry: mockExpire,
LifetimeDeadline: mockExpire,
RefreshDeadline: mockExpire,
}, nil)
tests := []struct {
name string
idToken string
want *RedeemResponse
want *sessions.SessionState
wantErr bool
}{
{"good", "unit_test", &RedeemResponse{
{"good", "unit_test", &sessions.SessionState{
AccessToken: "mocked access token",
RefreshToken: "mocked refresh token",
IDToken: "mocked id token",
User: "user1",
Email: "test@email.com",
Expiry: (fixedDate),
LifetimeDeadline: (fixedDate),
RefreshDeadline: (fixedDate),
}, false},
{"empty code", "", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := AuthenticateGRPC{client: mockAuthenticateClient}
got, err := a.Redeem(tt.idToken)
got, err := a.Redeem(context.Background(), tt.idToken)
if (err != nil) != tt.wantErr {
t.Errorf("Proxy.AuthenticateValidate() error = %v,\n wantErr %v", err, tt.wantErr)
return
@ -123,7 +127,7 @@ func TestProxy_AuthenticateValidate(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
a := AuthenticateGRPC{client: ac}
got, err := a.Validate(tt.idToken)
got, err := a.Validate(context.Background(), tt.idToken)
if (err != nil) != tt.wantErr {
t.Errorf("Proxy.AuthenticateValidate() error = %v, wantErr %v", err, tt.wantErr)
return
@ -139,43 +143,43 @@ 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)
}
mockExpire, _ := ptypes.TimestampProto(fixedDate)
mockRefreshClient.EXPECT().Refresh(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.RefreshReply{
AccessToken: "mocked access token",
Expiry: mockExpire,
gomock.Not(sessions.SessionState{RefreshToken: "fail"}),
).Return(&pb.Session{
AccessToken: "new access token",
RefreshDeadline: mockExpire,
LifetimeDeadline: mockExpire,
}, nil).AnyTimes()
tests := []struct {
name string
refreshToken string
wantAT string
wantExp time.Time
session *sessions.SessionState
want *sessions.SessionState
wantErr bool
}{
{"good", "unit_test", "mocked access token", fixedDate, false},
{"missing refresh", "", "", time.Time{}, true},
{"good",
&sessions.SessionState{RefreshToken: "unit_test"},
&sessions.SessionState{
AccessToken: "new access token",
RefreshDeadline: fixedDate,
LifetimeDeadline: fixedDate,
}, false},
{"empty refresh token", &sessions.SessionState{RefreshToken: ""}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := AuthenticateGRPC{client: mockRefreshClient}
got, gotExp, err := a.Refresh(tt.refreshToken)
got, err := a.Refresh(context.Background(), tt.session)
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)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Proxy.AuthenticateRefresh() got = \n%#v\nwant \n%#v", got, tt.want)
}
})
}
@ -195,8 +199,10 @@ func TestNewGRPC(t *testing.T) {
{"internal addr with port", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local:8443", SharedSecret: "shh"}, false, "", "intranet.local:8443"},
{"internal addr without port", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", SharedSecret: "shh"}, false, "", "intranet.local:443"},
{"cert override", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh"}, false, "", "intranet.local:443"},
// {"addr and internal ", &Options{Addr: "localhost", InternalAddr: "local.localhost", SharedSecret: "shh"}, nil, true, ""},
{"custom ca", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZrQ0ZBWHhneFg5K0hjWlBVVVBEK0laV0NGNUEvVTdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1FVXgKQ3pBSkJnTlZCQVlUQWtGVk1STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbApjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3SGhjTk1Ua3dNakk0TVRnMU1EQTNXaGNOTWprd01qSTFNVGcxCk1EQTNXakJGTVFzd0NRWURWUVFHRXdKQlZURVRNQkVHQTFVRUNBd0tVMjl0WlMxVGRHRjBaVEVoTUI4R0ExVUUKQ2d3WVNXNTBaWEp1WlhRZ1YybGtaMmwwY3lCUWRIa2dUSFJrTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBOVRFMEFiaTdnMHhYeURkVUtEbDViNTBCT05ZVVVSc3F2THQrSWkwdlpjMzRRTHhOClJrT0hrOFZEVUgzcUt1N2UrNGVubUdLVVNUdzRPNFlkQktiSWRJTFpnb3o0YitNL3FVOG5adVpiN2pBVTdOYWkKajMzVDVrbXB3L2d4WHNNUzNzdUpXUE1EUDB3Z1BUZUVRK2J1bUxVWmpLdUVIaWNTL0l5dmtaVlBzRlE4NWlaUwpkNXE2a0ZGUUdjWnFXeFg0dlhDV25Sd3E3cHY3TThJd1RYc1pYSVRuNXB5Z3VTczNKb29GQkg5U3ZNTjRKU25GCmJMK0t6ekduMy9ScXFrTXpMN3FUdkMrNWxVT3UxUmNES21mZXBuVGVaN1IyVnJUQm42NndWMjVHRnBkSDIzN00KOXhJVkJrWEd1U2NvWHVPN1lDcWFrZkt6aXdoRTV4UmRaa3gweXdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQQpBNElCQVFCaHRWUEI0OCs4eFZyVmRxM1BIY3k5QkxtVEtrRFl6N2Q0ODJzTG1HczBuVUdGSTFZUDdmaFJPV3ZxCktCTlpkNEI5MUpwU1NoRGUrMHpoNno4WG5Ha01mYnRSYWx0NHEwZ3lKdk9hUWhqQ3ZCcSswTFk5d2NLbXpFdnMKcTRiNUZ5NXNpRUZSekJLTmZtTGwxTTF2cW1hNmFCVnNYUUhPREdzYS83dE5MalZ2ay9PYm52cFg3UFhLa0E3cQpLMTQvV0tBRFBJWm9mb00xMzB4Q1RTYXVpeXROajlnWkx1WU9leEZhblVwNCt2MHBYWS81OFFSNTk2U0ROVTlKClJaeDhwTzBTaUYvZXkxVUZXbmpzdHBjbTQzTFVQKzFwU1hFeVhZOFJrRTI2QzNvdjNaTFNKc2pMbC90aXVqUlgKZUJPOWorWDdzS0R4amdtajBPbWdpVkpIM0YrUAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}, false, "", "intranet.local:443"},
{"bad ca encoding", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "^"}, true, "", "intranet.local:443"},
{"custom ca file", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt"}, false, "", "intranet.local:443"},
{"bad custom ca file", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt2"}, true, "", "intranet.local:443"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View file

@ -1,15 +1,16 @@
package authenticator // import "github.com/pomerium/pomerium/proxy/authenticator"
import (
"time"
"context"
"github.com/pomerium/pomerium/internal/sessions"
)
// MockAuthenticate provides a mocked implementation of the authenticator interface.
type MockAuthenticate struct {
RedeemError error
RedeemResponse *RedeemResponse
RefreshResponse string
RefreshTime time.Time
RedeemResponse *sessions.SessionState
RefreshResponse *sessions.SessionState
RefreshError error
ValidateResponse bool
ValidateError error
@ -17,17 +18,17 @@ type MockAuthenticate struct {
}
// Redeem is a mocked authenticator client function.
func (a MockAuthenticate) Redeem(code string) (*RedeemResponse, error) {
func (a MockAuthenticate) Redeem(ctx context.Context, code string) (*sessions.SessionState, error) {
return a.RedeemResponse, a.RedeemError
}
// Refresh is a mocked authenticator client function.
func (a MockAuthenticate) Refresh(refreshToken string) (string, time.Time, error) {
return a.RefreshResponse, a.RefreshTime, a.RefreshError
func (a MockAuthenticate) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) {
return a.RefreshResponse, a.RefreshError
}
// Validate is a mocked authenticator client function.
func (a MockAuthenticate) Validate(idToken string) (bool, error) {
func (a MockAuthenticate) Validate(ctx context.Context, idToken string) (bool, error) {
return a.ValidateResponse, a.ValidateError
}

View file

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDETCCAfkCFAXxgxX9+HcZPUUPD+IZWCF5A/U7MA0GCSqGSIb3DQEBCwUAMEUx
CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl
cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTkwMjI4MTg1MDA3WhcNMjkwMjI1MTg1
MDA3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE
CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEA9TE0Abi7g0xXyDdUKDl5b50BONYUURsqvLt+Ii0vZc34QLxN
RkOHk8VDUH3qKu7e+4enmGKUSTw4O4YdBKbIdILZgoz4b+M/qU8nZuZb7jAU7Nai
j33T5kmpw/gxXsMS3suJWPMDP0wgPTeEQ+bumLUZjKuEHicS/IyvkZVPsFQ85iZS
d5q6kFFQGcZqWxX4vXCWnRwq7pv7M8IwTXsZXITn5pyguSs3JooFBH9SvMN4JSnF
bL+KzzGn3/RqqkMzL7qTvC+5lUOu1RcDKmfepnTeZ7R2VrTBn66wV25GFpdH237M
9xIVBkXGuScoXuO7YCqakfKziwhE5xRdZkx0ywIDAQABMA0GCSqGSIb3DQEBCwUA
A4IBAQBhtVPB48+8xVrVdq3PHcy9BLmTKkDYz7d482sLmGs0nUGFI1YP7fhROWvq
KBNZd4B91JpSShDe+0zh6z8XnGkMfbtRalt4q0gyJvOaQhjCvBq+0LY9wcKmzEvs
q4b5Fy5siEFRzBKNfmLl1M1vqma6aBVsXQHODGsa/7tNLjVvk/ObnvpX7PXKkA7q
K14/WKADPIZofoM130xCTSauiytNj9gZLuYOexFanUp4+v0pXY/58QR596SDNU9J
RZx8pO0SiF/ey1UFWnjstpcm43LUP+1pSXEyXY8RkE26C3ov3ZLSJsjLl/tiujRX
eBO9j+X7sKDxjgmj0OmgiVJH3F+P
-----END CERTIFICATE-----

View file

@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"reflect"
"strings"
"time"
"github.com/pomerium/pomerium/internal/cryptutil"
@ -42,6 +43,7 @@ func (p *Proxy) Handler() http.Handler {
mux.HandleFunc("/robots.txt", p.RobotsTxt)
mux.HandleFunc("/.pomerium/sign_out", p.SignOut)
mux.HandleFunc("/.pomerium/callback", p.OAuthCallback)
// mux.HandleFunc("/.pomerium/refresh", p.Refresh) //todo(bdd): needs DoS protection before inclusion
mux.HandleFunc("/", p.Proxy)
// middleware chain
@ -159,7 +161,7 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
return
}
// We begin the process of redeeming the code for an access token.
rr, err := p.AuthenticateClient.Redeem(r.Form.Get("code"))
session, err := p.AuthenticateClient.Redeem(r.Context(), r.Form.Get("code"))
if err != nil {
log.FromRequest(r).Error().Err(err).Msg("proxy: error redeeming authorization code")
httputil.ErrorResponse(w, r, "Internal error", http.StatusInternalServerError)
@ -203,15 +205,7 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
}
// We store the session in a cookie and redirect the user back to the application
err = p.sessionStore.SaveSession(w, r,
&sessions.SessionState{
AccessToken: rr.AccessToken,
RefreshToken: rr.RefreshToken,
IDToken: rr.IDToken,
User: rr.User,
Email: rr.Email,
RefreshDeadline: (rr.Expiry).Truncate(time.Second),
})
err = p.sessionStore.SaveSession(w, r, session)
if err != nil {
log.FromRequest(r).Error().Msg("error saving session")
httputil.ErrorResponse(w, r, "Error saving session", http.StatusInternalServerError)
@ -221,8 +215,8 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
log.FromRequest(r).Info().
Str("code", r.Form.Get("code")).
Str("state", r.Form.Get("state")).
Str("RefreshToken", rr.RefreshToken).
Str("session", rr.AccessToken).
Str("RefreshToken", session.RefreshToken).
Str("session", session.AccessToken).
Str("RedirectURI", stateParameter.RedirectURI).
Msg("session")
@ -260,18 +254,40 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
route.ServeHTTP(w, r)
}
// Refresh refreshes a user session, validating group, extending timeout period, without requiring
// a user to re-authenticate
// func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) {
// session, err := p.sessionStore.LoadSession(r)
// if err != nil {
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
// return
// }
// session, err = p.AuthenticateClient.Refresh(r.Context(), session)
// if err != nil {
// log.FromRequest(r).Warn().Err(err).Msg("proxy: refresh failed")
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
// return
// }
// err = p.sessionStore.SaveSession(w, r, session)
// if err != nil {
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
// return
// }
// w.WriteHeader(http.StatusOK)
// jsonSession, err := json.Marshal(session)
// if err != nil {
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
// return
// }
// fmt.Fprintf(w, string(jsonSession))
// }
// Authenticate authenticates a request by checking for a session cookie, and validating its expiration,
// clearing the session cookie if it's invalid and returning an error if necessary..
func (p *Proxy) Authenticate(w http.ResponseWriter, r *http.Request) (err error) {
// Clear the session cookie if anything goes wrong.
defer func() {
if err != nil {
p.sessionStore.ClearSession(w, r)
}
}()
session, err := p.sessionStore.LoadSession(r)
if err != nil {
p.sessionStore.ClearSession(w, r)
return err
}
@ -279,26 +295,23 @@ func (p *Proxy) Authenticate(w http.ResponseWriter, r *http.Request) (err error)
// 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.AuthenticateClient.Refresh(session.RefreshToken)
session, err = p.AuthenticateClient.Refresh(r.Context(), session)
if err != nil {
log.FromRequest(r).Warn().
Str("RefreshToken", session.RefreshToken).
Str("AccessToken", session.AccessToken).
Msg("proxy: refresh failed")
p.sessionStore.ClearSession(w, r)
log.FromRequest(r).Warn().Err(err).Msg("proxy: refresh failed")
return err
}
session.AccessToken = accessToken
session.RefreshDeadline = expiry
log.FromRequest(r).Info().Msg("proxy: refresh success")
}
err = p.sessionStore.SaveSession(w, r, session)
if err != nil {
p.sessionStore.ClearSession(w, r)
return err
}
// pass user & user-email details to client applications
r.Header.Set(HeaderUserID, session.User)
r.Header.Set(HeaderEmail, session.Email)
r.Header.Set(HeaderGroups, strings.Join(session.Groups, ","))
// This user has been OK'd. Allow the request!
return nil
}

View file

@ -230,10 +230,10 @@ func TestProxy_OAuthCallback(t *testing.T) {
},
}
normalAuth := authenticator.MockAuthenticate{
RedeemResponse: &authenticator.RedeemResponse{
RedeemResponse: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
Expiry: time.Now().Add(10 * time.Second),
LifetimeDeadline: time.Now().Add(10 * time.Second),
},
}
normalCsrf := sessions.MockCSRFStore{
@ -390,19 +390,20 @@ func TestProxy_Proxy(t *testing.T) {
}
func TestProxy_Authenticate(t *testing.T) {
goodSession := &sessions.SessionState{
User: "user",
Email: "email@email.com",
Groups: []string{"group1"},
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(10 * time.Second),
}
expiredDeadline := &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(-10 * time.Second),
testAuth := authenticator.MockAuthenticate{
RedeemResponse: goodSession,
}
tests := []struct {
name string
host string
@ -414,30 +415,84 @@ func TestProxy_Authenticate(t *testing.T) {
{"cannot save session",
"https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"},
&sessions.MockSessionStore{Session: goodSession, SaveError: errors.New("error")},
authenticator.MockAuthenticate{}, true},
&sessions.MockSessionStore{Session: &sessions.SessionState{
User: "user",
Email: "email@email.com",
Groups: []string{"group1"},
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(10 * time.Second),
}, SaveError: errors.New("error")},
testAuth, true},
{"cannot load session",
"https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"},
&sessions.MockSessionStore{LoadError: errors.New("error")}, authenticator.MockAuthenticate{}, true},
&sessions.MockSessionStore{LoadError: errors.New("error")}, testAuth, true},
{"expired session",
"https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"},
&sessions.MockSessionStore{Session: expiredDeadline}, authenticator.MockAuthenticate{}, false},
&sessions.MockSessionStore{
Session: &sessions.SessionState{
User: "user",
Email: "email@email.com",
Groups: []string{"group1"},
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(-10 * time.Second),
}},
authenticator.MockAuthenticate{
RefreshError: errors.New("error"),
RefreshResponse: &sessions.SessionState{
User: "user",
Email: "email@email.com",
Groups: []string{"group1"},
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(-10 * time.Second),
}}, true},
{"bad refresh authenticator",
"https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"},
&sessions.MockSessionStore{
Session: expiredDeadline,
Session: &sessions.SessionState{
User: "user",
Email: "email@email.com",
Groups: []string{"group1"},
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(-10 * time.Second),
},
authenticator.MockAuthenticate{RefreshError: errors.New("error")},
},
authenticator.MockAuthenticate{
RefreshError: errors.New("error"),
RefreshResponse: &sessions.SessionState{
User: "user",
Email: "email@email.com",
Groups: []string{"group1"},
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(-10 * time.Second),
}},
true},
{"good",
"https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"},
&sessions.MockSessionStore{Session: goodSession}, authenticator.MockAuthenticate{}, false},
&sessions.MockSessionStore{Session: &sessions.SessionState{
User: "user",
Email: "email@email.com",
Groups: []string{"group1"},
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(10 * time.Second),
}}, testAuth, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -452,6 +507,7 @@ func TestProxy_Authenticate(t *testing.T) {
p.cipher = mockCipher{}
r := httptest.NewRequest("GET", tt.host, nil)
w := httptest.NewRecorder()
fmt.Printf("%s", tt.name)
if err := p.Authenticate(w, r); (err != nil) != tt.wantErr {
t.Errorf("Proxy.Authenticate() error = %v, wantErr %v", err, tt.wantErr)

View file

@ -28,6 +28,8 @@ const (
HeaderUserID = "x-pomerium-authenticated-user-id"
// HeaderEmail represents the header key for the email that is passed to the client.
HeaderEmail = "x-pomerium-authenticated-user-email"
// HeaderGroups represents the header key for the groups that is passed to the client.
HeaderGroups = "x-pomerium-authenticated-user-groups"
)
// Options represents the configurations available for the proxy service.
@ -54,7 +56,6 @@ type Options struct {
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
CookieLifetimeTTL time.Duration `envconfig:"COOKIE_LIFETIME"`
// Sub-routes
Routes map[string]string `envconfig:"ROUTES"`
@ -66,9 +67,8 @@ var defaultOptions = &Options{
CookieName: "_pomerium_proxy",
CookieHTTPOnly: true,
CookieSecure: true,
CookieExpire: time.Duration(168) * time.Hour,
CookieExpire: time.Duration(14) * time.Hour,
CookieRefresh: time.Duration(30) * time.Minute,
CookieLifetimeTTL: time.Duration(720) * time.Hour,
DefaultUpstreamTimeout: time.Duration(10) * time.Second,
// services
AuthenticatePort: 443,
@ -247,11 +247,14 @@ func deleteUpstreamCookies(req *http.Request, cookieName string) {
req.Header.Set("Cookie", strings.Join(headers, ";"))
}
func (u *UpstreamProxy) signRequest(req *http.Request) {
func (u *UpstreamProxy) signRequest(r *http.Request) {
if u.signer != nil {
jwt, err := u.signer.SignJWT(req.Header.Get(HeaderUserID), req.Header.Get(HeaderEmail))
jwt, err := u.signer.SignJWT(
r.Header.Get(HeaderUserID),
r.Header.Get(HeaderEmail),
r.Header.Get(HeaderGroups))
if err == nil {
req.Header.Set(HeaderJWT, jwt)
r.Header.Set(HeaderJWT, jwt)
}
}
}