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
35
CHANGELOG.md
Normal 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:
|
||||
|
||||
*
|
2
Makefile
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -50,11 +50,12 @@ type Options struct {
|
|||
|
||||
// IdentityProvider provider configuration variables as specified by RFC6749
|
||||
// https://openid.net/specs/openid-connect-basic-1_0.html#RFC6749
|
||||
ClientID string `envconfig:"IDP_CLIENT_ID"`
|
||||
ClientSecret string `envconfig:"IDP_CLIENT_SECRET"`
|
||||
Provider string `envconfig:"IDP_PROVIDER"`
|
||||
ProviderURL string `envconfig:"IDP_PROVIDER_URL"`
|
||||
Scopes []string `envconfig:"IDP_SCOPES"`
|
||||
ClientID string `envconfig:"IDP_CLIENT_ID"`
|
||||
ClientSecret string `envconfig:"IDP_CLIENT_SECRET"`
|
||||
Provider string `envconfig:"IDP_PROVIDER"`
|
||||
ProviderURL string `envconfig:"IDP_PROVIDER_URL"`
|
||||
Scopes []string `envconfig:"IDP_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{
|
||||
RedirectURL: opts.RedirectURL,
|
||||
ProviderName: opts.Provider,
|
||||
ProviderURL: opts.ProviderURL,
|
||||
ClientID: opts.ClientID,
|
||||
ClientSecret: opts.ClientSecret,
|
||||
Scopes: opts.Scopes,
|
||||
&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
|
||||
|
|
|
@ -19,9 +19,8 @@ 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",
|
||||
CookieExpire: time.Duration(168) * time.Hour,
|
||||
CookieName: "pomerium",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
@ -78,24 +52,43 @@ func TestAuthenticate_Refresh(t *testing.T) {
|
|||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
refreshToken string
|
||||
want *pb.RefreshReply
|
||||
wantErr bool
|
||||
name string
|
||||
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,12 +125,13 @@ func TestAuthenticate_Authenticate(t *testing.T) {
|
|||
User: "user",
|
||||
}
|
||||
|
||||
goodReply := &pb.AuthenticateReply{
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
Expiry: vtProto,
|
||||
Email: "user@domain.com",
|
||||
User: "user"}
|
||||
goodReply := &pb.Session{
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
LifetimeDeadline: vtProto,
|
||||
RefreshDeadline: vtProto,
|
||||
Email: "user@domain.com",
|
||||
User: "user"}
|
||||
ciphertext, err := sessions.MarshalSession(want, c)
|
||||
if err != nil {
|
||||
t.Fatalf("expected to be encode session: %v", err)
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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{
|
||||
AccessToken: "new token",
|
||||
Expiry: time.Now(),
|
||||
RefreshResponse: &sessions.SessionState{
|
||||
AccessToken: "new token",
|
||||
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{
|
||||
AccessToken: "new token",
|
||||
Expiry: time.Now(),
|
||||
RefreshResponse: &sessions.SessionState{
|
||||
AccessToken: "new token",
|
||||
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",
|
||||
|
|
|
@ -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"
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
BIN
docs/docs/google/google-create-sa.png
Normal file
After Width: | Height: | Size: 284 KiB |
BIN
docs/docs/google/google-gsuite-add-scopes.png
Normal file
After Width: | Height: | Size: 236 KiB |
|
@ -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
|
|||
|
||||

|
||||
|
||||
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.  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**.
|
||||
|
||||

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

|
||||
|
||||
Click on **Endpoints**
|
||||
|
||||

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

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

|
||||
|
||||
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**.
|
||||
|
||||

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

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

|
||||
|
||||
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**.
|
||||
|
||||

|
||||
|
||||
Select your desired authorization server and navigate to the **claims tab**. Click **Add Claim** and configure the group claim for **ID Token** as follows.
|
||||
|
||||

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

|
||||
|
||||
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"
|
||||
|
@ -227,15 +321,26 @@ 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.
|
||||
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.
|
||||
|
||||
|
||||

|
||||
|
||||
[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**
|
||||
|
||||

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

|
||||
|
||||
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"
|
||||
|
|
BIN
docs/docs/microsoft/azure-add-groups-claim.png
Normal file
After Width: | Height: | Size: 393 KiB |
BIN
docs/docs/microsoft/azure-api-settings.png
Normal file
After Width: | Height: | Size: 297 KiB |
BIN
docs/docs/okta/okta-authorization-servers.png
Normal file
After Width: | Height: | Size: 216 KiB |
BIN
docs/docs/okta/okta-configure-groups-claim.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
docs/docs/okta/okta-list-groups-claim.png
Normal file
After Width: | Height: | Size: 310 KiB |
BIN
docs/docs/one-login/one-login-oidc-groups-param.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
docs/docs/one-login/one-login-oidc-params.png
Normal file
After Width: | Height: | Size: 44 KiB |
|
@ -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.
|
||||
[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
|
@ -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
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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
|
||||
}
|
188
internal/identity/microsoft.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
@ -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
|
||||
}
|
142
internal/identity/onelogin.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
//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)
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
58
proto/authenticate/convert.go
Normal 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
|
||||
}
|
|
@ -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{
|
||||
AccessToken: "mocked access token",
|
||||
RefreshToken: "mocked refresh token",
|
||||
IdToken: "mocked id token",
|
||||
User: "user1",
|
||||
Email: "test@email.com",
|
||||
Expiry: mockExpire,
|
||||
).Return(&pb.Session{
|
||||
AccessToken: "mocked access token",
|
||||
RefreshToken: "mocked refresh token",
|
||||
IdToken: "mocked id token",
|
||||
User: "user1",
|
||||
Email: "test@email.com",
|
||||
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{
|
||||
AccessToken: "mocked access token",
|
||||
Expiry: mockExpire,
|
||||
).Return(&pb.Session{
|
||||
AccessToken: "mocked access token",
|
||||
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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
Expiry: fixedDate,
|
||||
redeemResponse := &sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
LifetimeDeadline: fixedDate,
|
||||
}
|
||||
ma := &MockAuthenticate{
|
||||
RedeemError: errors.New("RedeemError"),
|
||||
RedeemResponse: redeemResponse,
|
||||
RefreshResponse: "RefreshResponse",
|
||||
RefreshTime: fixedDate,
|
||||
RedeemError: errors.New("RedeemError"),
|
||||
RedeemResponse: redeemResponse,
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
AccessToken: "mocked access token",
|
||||
RefreshToken: "mocked refresh token",
|
||||
IdToken: "mocked id token",
|
||||
User: "user1",
|
||||
Email: "test@email.com",
|
||||
Expiry: mockExpire,
|
||||
).Return(&pb.Session{
|
||||
AccessToken: "mocked access token",
|
||||
RefreshToken: "mocked refresh token",
|
||||
IdToken: "mocked id token",
|
||||
User: "user1",
|
||||
Email: "test@email.com",
|
||||
LifetimeDeadline: mockExpire,
|
||||
RefreshDeadline: mockExpire,
|
||||
}, nil)
|
||||
tests := []struct {
|
||||
name string
|
||||
idToken string
|
||||
want *RedeemResponse
|
||||
want *sessions.SessionState
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", "unit_test", &RedeemResponse{
|
||||
AccessToken: "mocked access token",
|
||||
RefreshToken: "mocked refresh token",
|
||||
IDToken: "mocked id token",
|
||||
User: "user1",
|
||||
Email: "test@email.com",
|
||||
Expiry: (fixedDate),
|
||||
{"good", "unit_test", &sessions.SessionState{
|
||||
AccessToken: "mocked access token",
|
||||
RefreshToken: "mocked refresh token",
|
||||
IDToken: "mocked id token",
|
||||
User: "user1",
|
||||
Email: "test@email.com",
|
||||
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
|
||||
wantErr bool
|
||||
name string
|
||||
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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
19
proxy/authenticator/testdata/example.crt
vendored
Normal 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-----
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -230,10 +230,10 @@ func TestProxy_OAuthCallback(t *testing.T) {
|
|||
},
|
||||
}
|
||||
normalAuth := authenticator.MockAuthenticate{
|
||||
RedeemResponse: &authenticator.RedeemResponse{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
Expiry: time.Now().Add(10 * time.Second),
|
||||
RedeemResponse: &sessions.SessionState{
|
||||
AccessToken: "AccessToken",
|
||||
RefreshToken: "RefreshToken",
|
||||
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)
|
||||
|
|
|
@ -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.
|
||||
|
@ -47,14 +49,13 @@ type Options struct {
|
|||
SharedKey string `envconfig:"SHARED_SECRET"`
|
||||
|
||||
// Session/Cookie management
|
||||
CookieName string
|
||||
CookieSecret string `envconfig:"COOKIE_SECRET"`
|
||||
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
|
||||
CookieSecure bool `envconfig:"COOKIE_SECURE"`
|
||||
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
|
||||
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
|
||||
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
|
||||
CookieLifetimeTTL time.Duration `envconfig:"COOKIE_LIFETIME"`
|
||||
CookieName string
|
||||
CookieSecret string `envconfig:"COOKIE_SECRET"`
|
||||
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
|
||||
CookieSecure bool `envconfig:"COOKIE_SECURE"`
|
||||
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
|
||||
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
|
||||
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|