mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-29 18:36:30 +02:00
* authenticate: validate origin of signout - add a debug task to kill envoy - improve various function docs - userinfo: return "error" page if user is logged out without redirect uri set - remove front channel logout. There's little difference between it, and the signout function. Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com> Co-authored-by: bobby <1544881+desimone@users.noreply.github.com>
This commit is contained in:
parent
41d0522da1
commit
edb3533d87
11 changed files with 174 additions and 182 deletions
|
@ -48,13 +48,20 @@ func (a *Authenticate) Mount(r *mux.Router) {
|
|||
r.Use(func(h http.Handler) http.Handler {
|
||||
options := a.options.Load()
|
||||
state := a.state.Load()
|
||||
csrfKey := fmt.Sprintf("%s_csrf", options.CookieName)
|
||||
return csrf.Protect(
|
||||
state.cookieSecret,
|
||||
csrf.Secure(options.CookieSecure),
|
||||
csrf.Path("/"),
|
||||
csrf.UnsafePaths([]string{state.redirectURL.Path}), // enforce CSRF on "safe" handler
|
||||
csrf.FormValueName("state"), // rfc6749 section-10.12
|
||||
csrf.CookieName(fmt.Sprintf("%s_csrf", options.CookieName)),
|
||||
csrf.UnsafePaths(
|
||||
[]string{
|
||||
"/oauth2/callback", // rfc6749#section-10.12 accepts GET
|
||||
"/.pomerium/sign_out", // https://openid.net/specs/openid-connect-frontchannel-1_0.html
|
||||
}),
|
||||
csrf.FormValueName("state"), // rfc6749#section-10.12
|
||||
csrf.CookieName(csrfKey),
|
||||
csrf.FieldName(csrfKey),
|
||||
csrf.SameSite(csrf.SameSiteLaxMode),
|
||||
csrf.ErrorHandler(httputil.HandlerFunc(httputil.CSRFFailureHandler)),
|
||||
)(h)
|
||||
})
|
||||
|
@ -63,11 +70,6 @@ func (a *Authenticate) Mount(r *mux.Router) {
|
|||
// Identity Provider (IdP) endpoints
|
||||
r.Path("/oauth2/callback").Handler(httputil.HandlerFunc(a.OAuthCallback)).Methods(http.MethodGet)
|
||||
|
||||
// Proxy service endpoints
|
||||
s := r.PathPrefix("/.pomerium/frontchannel-logout").Subrouter()
|
||||
s.Use(a.RetrieveSession)
|
||||
s.Path("/").Handler(httputil.HandlerFunc(a.FrontchannelLogout)).Methods(http.MethodGet)
|
||||
|
||||
v := r.PathPrefix("/.pomerium").Subrouter()
|
||||
c := cors.New(cors.Options{
|
||||
AllowOriginRequestFunc: func(r *http.Request, _ string) bool {
|
||||
|
@ -93,26 +95,29 @@ func (a *Authenticate) Mount(r *mux.Router) {
|
|||
wk.Path("/").Handler(httputil.HandlerFunc(a.wellKnown)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
// Well-Known Uniform Resource Identifiers (URIs)
|
||||
// wellKnown returns a list of well known URLS for Pomerium.
|
||||
//
|
||||
// https://en.wikipedia.org/wiki/List_of_/.well-known/_services_offered_by_webservers
|
||||
func (a *Authenticate) wellKnown(w http.ResponseWriter, r *http.Request) error {
|
||||
state := a.state.Load()
|
||||
|
||||
wellKnownURLS := struct {
|
||||
// URL string referencing the client's JSON Web Key (JWK) Set
|
||||
// RFC7517 document, which contains the client's public keys.
|
||||
JSONWebKeySetURL string `json:"jwks_uri"`
|
||||
OAuth2Callback string `json:"authentication_callback_endpoint"`
|
||||
FrontchannelLogoutURI string `json:"frontchannel_logout_uri"`
|
||||
OAuth2Callback string `json:"authentication_callback_endpoint"` // RFC6749
|
||||
JSONWebKeySetURL string `json:"jwks_uri"` // RFC7517
|
||||
FrontchannelLogoutURI string `json:"frontchannel_logout_uri"` // https://openid.net/specs/openid-connect-frontchannel-1_0.html
|
||||
}{
|
||||
state.redirectURL.ResolveReference(&url.URL{Path: "/.well-known/pomerium/jwks.json"}).String(),
|
||||
state.redirectURL.ResolveReference(&url.URL{Path: "/oauth2/callback"}).String(),
|
||||
state.redirectURL.ResolveReference(&url.URL{Path: "/.pomerium/frontchannel-logout"}).String(),
|
||||
state.redirectURL.ResolveReference(&url.URL{Path: "/.well-known/pomerium/jwks.json"}).String(),
|
||||
state.redirectURL.ResolveReference(&url.URL{Path: "/.pomerium/sign_out"}).String(),
|
||||
}
|
||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||
httputil.RenderJSON(w, http.StatusOK, wellKnownURLS)
|
||||
return nil
|
||||
}
|
||||
|
||||
// jwks returns the signing key(s) the client can use to validate signatures
|
||||
// from the authorization server.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc8414
|
||||
func (a *Authenticate) jwks(w http.ResponseWriter, r *http.Request) error {
|
||||
httputil.RenderJSON(w, http.StatusOK, a.state.Load().jwk)
|
||||
return nil
|
||||
|
@ -262,11 +267,12 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) error {
|
|||
} else if !errors.Is(err, oidc.ErrSignoutNotImplemented) {
|
||||
log.Warn().Err(err).Msg("authenticate.SignOut: failed getting session")
|
||||
}
|
||||
|
||||
if redirectString != "" {
|
||||
httputil.Redirect(w, r, redirectString, http.StatusFound)
|
||||
|
||||
return nil
|
||||
}
|
||||
return httputil.NewError(http.StatusOK, errors.New("user logged out"))
|
||||
}
|
||||
|
||||
// reauthenticateOrFail starts the authenticate process by redirecting the
|
||||
// user to their respective identity provider. This function also builds the
|
||||
|
@ -425,11 +431,6 @@ func (a *Authenticate) getSessionFromCtx(ctx context.Context) (*sessions.State,
|
|||
return &s, nil
|
||||
}
|
||||
|
||||
func (a *Authenticate) deleteSession(ctx context.Context, sessionID string) error {
|
||||
state := a.state.Load()
|
||||
return session.Delete(ctx, state.dataBrokerClient, sessionID)
|
||||
}
|
||||
|
||||
func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx, span := trace.StartSpan(r.Context(), "authenticate.userInfo")
|
||||
defer span.End()
|
||||
|
@ -479,17 +480,8 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
|||
"DirectoryGroups": groups, // user's groups inferred from idp directory
|
||||
"csrfField": csrf.TemplateField(r),
|
||||
"RedirectURL": r.URL.Query().Get(urlutil.QueryRedirectURI),
|
||||
"SignOutURL": "/.pomerium/sign_out",
|
||||
}
|
||||
|
||||
if redirectURL, err := url.Parse(r.URL.Query().Get(urlutil.QueryRedirectURI)); err == nil {
|
||||
input["RedirectURL"] = redirectURL.String()
|
||||
signOutURL := redirectURL.ResolveReference(new(url.URL))
|
||||
signOutURL.Path = "/.pomerium/sign_out"
|
||||
input["SignOutURL"] = signOutURL.String()
|
||||
} else {
|
||||
input["SignOutURL"] = "/.pomerium/sign_out"
|
||||
}
|
||||
|
||||
return a.templates.ExecuteTemplate(w, "userInfo.html", input)
|
||||
}
|
||||
|
||||
|
@ -557,27 +549,6 @@ func (a *Authenticate) saveSessionToDataBroker(
|
|||
return nil
|
||||
}
|
||||
|
||||
// FrontchannelLogout uses HTTP GETs to Relying Party URLs (Pomerium) to clear a user's login state.
|
||||
// This endpoint implements OpenID Connect Front-Channel Logout and reuses the Relying
|
||||
// Party-initiated logout functionality specified in Section 5 of OpenID Connect Session Management
|
||||
// 1.0 (RP-Initiated Logout).
|
||||
//
|
||||
// https://openid.net/specs/openid-connect-frontchannel-1_0.html
|
||||
// https://ldapwiki.com/wiki/OpenID%20Connect%20Front-Channel%20Logout
|
||||
func (a *Authenticate) FrontchannelLogout(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx, span := trace.StartSpan(r.Context(), "authenticate.FrontchannelLogout")
|
||||
defer span.End()
|
||||
|
||||
_ = a.revokeSession(ctx, w, r)
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, http.StatusText(http.StatusOK))
|
||||
return nil
|
||||
}
|
||||
|
||||
// revokeSession always clears the local session and tries to revoke the associated session stored in the
|
||||
// databroker. If successful, it returns the original `id_token` of the session, if failed, returns
|
||||
// and empty string.
|
||||
|
@ -598,7 +569,7 @@ func (a *Authenticate) revokeSession(ctx context.Context, w http.ResponseWriter,
|
|||
log.Ctx(ctx).Warn().Err(err).Msg("authenticate: failed to revoke access token")
|
||||
}
|
||||
}
|
||||
if err := a.deleteSession(ctx, sessionState.ID); err != nil {
|
||||
if err := session.Delete(ctx, state.dataBrokerClient, sessionState.ID); err != nil {
|
||||
log.Ctx(ctx).Warn().Err(err).Msg("authenticate: failed to delete session from session store")
|
||||
}
|
||||
|
||||
|
|
|
@ -13,10 +13,18 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/oauth2"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/encoding"
|
||||
|
@ -33,15 +41,6 @@ import (
|
|||
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/oauth2"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
func testAuthenticate() *Authenticate {
|
||||
|
@ -106,7 +105,7 @@ func TestAuthenticate_Handler(t *testing.T) {
|
|||
expected = fmt.Sprintf("User-agent: *\nDisallow: /")
|
||||
code := rr.Code
|
||||
if code != http.StatusOK {
|
||||
t.Errorf("bad preflight code")
|
||||
t.Errorf("bad preflight code %v", code)
|
||||
}
|
||||
resp := rr.Result()
|
||||
body = resp.Header.Get("vary")
|
||||
|
@ -235,6 +234,7 @@ func TestAuthenticate_SignOut(t *testing.T) {
|
|||
{"failed revoke", http.MethodPost, nil, "https://corp.pomerium.io/", "", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""},
|
||||
{"load session error", http.MethodPost, errors.New("error"), "https://corp.pomerium.io/", "", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""},
|
||||
{"bad redirect uri", http.MethodPost, nil, "corp.pomerium.io/", "", "sig", "ts", identity.MockProvider{LogOutError: oidc.ErrSignoutNotImplemented}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""},
|
||||
{"no redirect uri", http.MethodPost, nil, "", "", "sig", "ts", identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusOK, "{\"Status\":200,\"Error\":\"OK: user logged out\"}\n"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -566,7 +566,7 @@ func TestWellKnownEndpoint(t *testing.T) {
|
|||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
body := rr.Body.String()
|
||||
expected := "{\"jwks_uri\":\"https://auth.example.com/.well-known/pomerium/jwks.json\",\"authentication_callback_endpoint\":\"https://auth.example.com/oauth2/callback\",\"frontchannel_logout_uri\":\"https://auth.example.com/.pomerium/frontchannel-logout\"}\n"
|
||||
expected := "{\"authentication_callback_endpoint\":\"https://auth.example.com/oauth2/callback\",\"jwks_uri\":\"https://auth.example.com/.well-known/pomerium/jwks.json\",\"frontchannel_logout_uri\":\"https://auth.example.com/.pomerium/sign_out\"}\n"
|
||||
assert.Equal(t, body, expected)
|
||||
}
|
||||
|
||||
|
@ -669,84 +669,6 @@ func TestAuthenticate_userInfo(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate_FrontchannelLogout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
logoutIssuer string
|
||||
tokenIssuer string
|
||||
widthSession bool
|
||||
sessionStore sessions.SessionStore
|
||||
provider identity.MockProvider
|
||||
|
||||
wantCode int
|
||||
}{
|
||||
{"good", "https://idp.pomerium.io", "https://idp.pomerium.io", true, &mstore.Store{}, identity.MockProvider{AuthenticateResponse: oauth2.Token{}}, http.StatusOK},
|
||||
{"good no session", "https://idp.pomerium.io", "https://idp.pomerium.io", false, &mstore.Store{SaveError: errors.New("error")}, identity.MockProvider{AuthenticateResponse: oauth2.Token{}}, http.StatusOK},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
a := &Authenticate{
|
||||
state: newAtomicAuthenticateState(&authenticateState{
|
||||
sessionStore: tt.sessionStore,
|
||||
encryptedEncoder: mock.Encoder{},
|
||||
sharedEncoder: mock.Encoder{},
|
||||
dataBrokerClient: mockDataBrokerServiceClient{
|
||||
delete: func(ctx context.Context, in *databroker.DeleteRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
return nil, nil
|
||||
},
|
||||
get: func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
|
||||
if !tt.widthSession {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
data, err := ptypes.MarshalAny(&session.Session{
|
||||
Id: "SESSION_ID",
|
||||
IdToken: &session.IDToken{
|
||||
Issuer: tt.tokenIssuer,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &databroker.GetResponse{
|
||||
Record: &databroker.Record{
|
||||
Version: "0001",
|
||||
Type: data.GetTypeUrl(),
|
||||
Id: "SESSION_ID",
|
||||
Data: data,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
directoryClient: new(mockDirectoryServiceClient),
|
||||
}),
|
||||
options: config.NewAtomicOptions(),
|
||||
provider: identity.NewAtomicAuthenticator(),
|
||||
}
|
||||
|
||||
a.provider.Store(tt.provider)
|
||||
u, _ := url.Parse("/.pomerium/frontchannel-logout")
|
||||
params, _ := url.ParseQuery(u.RawQuery)
|
||||
params.Add("iss", tt.logoutIssuer)
|
||||
u.RawQuery = params.Encode()
|
||||
r := httptest.NewRequest(http.MethodGet, u.String(), nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
httputil.HandlerFunc(a.FrontchannelLogout).ServeHTTP(w, r)
|
||||
if status := w.Code; status != tt.wantCode {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", status, tt.wantCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockDataBrokerServiceClient struct {
|
||||
databroker.DataBrokerServiceClient
|
||||
|
||||
|
@ -779,3 +701,87 @@ func (m mockDirectoryServiceClient) RefreshUser(ctx context.Context, in *directo
|
|||
}
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
func TestAuthenticate_SignOut_CSRF(t *testing.T) {
|
||||
now := time.Now()
|
||||
signer, err := jws.NewHS256Signer(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pbNow, _ := ptypes.TimestampProto(now)
|
||||
a := &Authenticate{
|
||||
options: config.NewAtomicOptions(),
|
||||
state: newAtomicAuthenticateState(&authenticateState{
|
||||
// sessionStore: tt.sessionStore,
|
||||
cookieSecret: cryptutil.NewKey(),
|
||||
encryptedEncoder: signer,
|
||||
sharedEncoder: signer,
|
||||
dataBrokerClient: mockDataBrokerServiceClient{
|
||||
get: func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
|
||||
data, err := ptypes.MarshalAny(&session.Session{
|
||||
Id: "SESSION_ID",
|
||||
UserId: "USER_ID",
|
||||
IdToken: &session.IDToken{IssuedAt: pbNow},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &databroker.GetResponse{
|
||||
Record: &databroker.Record{
|
||||
Version: "0001",
|
||||
Type: data.GetTypeUrl(),
|
||||
Id: "SESSION_ID",
|
||||
Data: data,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
directoryClient: new(mockDirectoryServiceClient),
|
||||
}),
|
||||
templates: template.Must(frontend.NewTemplates()),
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
setCSRFCookie bool
|
||||
method string
|
||||
wantStatus int
|
||||
wantBody string
|
||||
}{
|
||||
{"GET without CSRF should fail", false, "GET", 400, "{\"Status\":400,\"Error\":\"Bad Request: CSRF token invalid\"}\n"},
|
||||
{"POST without CSRF should fail", false, "POST", 400, "{\"Status\":400,\"Error\":\"Bad Request: CSRF token invalid\"}\n"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := a.Handler()
|
||||
|
||||
// Obtain a CSRF cookie via a GET request.
|
||||
orr, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
s.ServeHTTP(rr, orr)
|
||||
|
||||
r, err := http.NewRequest(tt.method, "/.pomerium/sign_out", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.setCSRFCookie {
|
||||
r.Header.Set("Cookie", rr.Header().Get("Set-Cookie"))
|
||||
}
|
||||
r.Header.Set("Accept", "application/json")
|
||||
r.Header.Set("Referer", "/")
|
||||
rr = httptest.NewRecorder()
|
||||
s.ServeHTTP(rr, r)
|
||||
|
||||
if rr.Code != tt.wantStatus {
|
||||
t.Errorf("status: got %v want %v", rr.Code, tt.wantStatus)
|
||||
}
|
||||
body := rr.Body.String()
|
||||
if diff := cmp.Diff(body, tt.wantBody); diff != "" {
|
||||
t.Errorf("handler returned wrong body Body: %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
title: Single Sign-out
|
||||
description: This article describes Pomerium's support for Single Sign-out according to OpenID Connect Front-Channel Logout 1.0.
|
||||
description: >-
|
||||
This article describes Pomerium's support for Single Sign-out according to
|
||||
OpenID Connect Front-Channel Logout 1.0.
|
||||
---
|
||||
|
||||
# Single Sign-out
|
||||
|
@ -23,4 +25,21 @@ To find out if your identity provider (IdP) supports Front-Channel Logout, have
|
|||
|
||||
### Configuration
|
||||
|
||||
You need to register a `frontchannel_logout_uri` in your OAuth 2.0 Client settings. The url gets handled by the Authenticate Service under the path `/.pomerium/frontchannel-logout` (e.g `https://authenticate.localhost.pomerium.io/.pomerium/frontchannel-logout`).
|
||||
You need to register a `frontchannel_logout_uri` in your OAuth 2.0 Client settings. The url gets handled by the Authenticate Service under the path `/.pomerium/sign_out` (e.g `https://authenticate.localhost.pomerium.io/.pomerium/sign_out`).
|
||||
|
||||
|
||||
### The endpoint
|
||||
|
||||
See Pomerium's `/.well-known/pomerium` endpoint for the sign-out page's uri. For example,
|
||||
|
||||
```json
|
||||
{
|
||||
"authentication_callback_endpoint": "https://authenticate.localhost.pomerium.io/oauth2/callback",
|
||||
"jwks_uri": "https://authenticate.localhost.pomerium.io/.well-known/pomerium/jwks.json",
|
||||
"frontchannel_logout_uri": "https://authenticate.localhost.pomerium.io/.pomerium/sign_out"
|
||||
}
|
||||
```
|
||||
|
||||
Note, a CSRF token is required for the single sign out endpoint (despite supporting `GET` and `POST`) and can be retrieved from the
|
||||
`X-CSRF-Token` response header on the well known endpoint above or using the `_pomerium_csrf` session set.
|
||||
|
||||
|
|
|
@ -19,6 +19,10 @@ With the v0.13 release, routes may contain [multiple `to` URLs](/reference/#to),
|
|||
See [Load Balancing](/docs/topics/load-balancing) for more information on using this feature set.
|
||||
## Breaking
|
||||
|
||||
### Sign-out endpoint requires CSRF Token
|
||||
|
||||
The frontchannel-logout endpoint will now require a CSRF token for both `GET` and `POST` requests.
|
||||
|
||||
### User impersonation removed
|
||||
|
||||
With the v0.13.0 release, user impersonation has been removed.
|
||||
|
|
2
go.mod
2
go.mod
|
@ -43,7 +43,7 @@ require (
|
|||
github.com/openzipkin/zipkin-go v0.2.5
|
||||
github.com/ory/dockertest/v3 v3.6.3
|
||||
github.com/pelletier/go-toml v1.8.0 // indirect
|
||||
github.com/pomerium/csrf v1.6.2-0.20190918035251-f3318380bad3
|
||||
github.com/pomerium/csrf v1.7.0
|
||||
github.com/prometheus/client_golang v1.9.0
|
||||
github.com/rakyll/statik v0.1.7
|
||||
github.com/rjeczalik/notify v0.9.3-0.20201210012515-e2a77dcc14cf
|
||||
|
|
4
go.sum
4
go.sum
|
@ -501,8 +501,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pomerium/csrf v1.6.2-0.20190918035251-f3318380bad3 h1:FmzFXnCAepHZwl6QPhTFqBHcbcGevdiEQjutK+M5bj4=
|
||||
github.com/pomerium/csrf v1.6.2-0.20190918035251-f3318380bad3/go.mod h1:UE2U4JOsjXNeq+MX/lqhZpUFsNAxbXERuYsWK2iULh0=
|
||||
github.com/pomerium/csrf v1.7.0 h1:Qp4t6oyEod3svQtKfJZs589mdUTWKVf7q0PgCKYCshY=
|
||||
github.com/pomerium/csrf v1.7.0/go.mod h1:hAPZV47mEj2T9xFs+ysbum4l7SF1IdrryYaY6PdoIqw=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
|
|
|
@ -12,7 +12,13 @@
|
|||
<div class="header clearfix">
|
||||
<div class="heading">
|
||||
<a href="{{.RedirectURL}}" class="logo"></a>
|
||||
<span><a class="button" href="{{.SignOutURL}}">Logout</a></span>
|
||||
<span>
|
||||
<form action="{{.SignOutURL}}" method="post">
|
||||
{{.csrfField}}
|
||||
<input type="hidden" name="pomerium_redirect_uri" value="{{.RedirectURL}}">
|
||||
<input class="button" type="submit" value="Logout"/>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
|
|
@ -458,6 +458,7 @@ table tbody tr:nth-child(2n + 1) td {
|
|||
background: #f6f9fc;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
a.button {
|
||||
background: #6e43e8;
|
||||
|
@ -468,22 +469,10 @@ a.button {
|
|||
color: #f6f9fc;
|
||||
font-weight: 500;
|
||||
padding: 0 12px;
|
||||
/* line-height: 32px; */
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: white;
|
||||
box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
||||
rgba(0, 0, 0, 0.12) 0px 1px 1px 0px, rgba(60, 66, 87, 0.16) 0px 0px 0px 1px,
|
||||
rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
||||
rgba(60, 66, 87, 0.12) 0px 2px 5px 0px;
|
||||
/* box-shadow: 0 2px 5px 0 rgba(50, 50, 93, .20), 0 1px 1px 0 rgba(0, 0, 0, .14); */
|
||||
color: var(--sail-color-text);
|
||||
margin-top: 2px;
|
||||
transition: box-shadow 150ms ease-in-out;
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -21,14 +21,13 @@ func (p *Proxy) registerDashboardHandlers(r *mux.Router) *mux.Router {
|
|||
h := r.PathPrefix(dashboardPath).Subrouter()
|
||||
h.Use(middleware.SetHeaders(httputil.HeadersContentSecurityPolicy))
|
||||
|
||||
// dashboard endpoints can be used by user's to view, or modify their session
|
||||
h.Path("/").HandlerFunc(p.UserDashboard).Methods(http.MethodGet)
|
||||
// special pomerium endpoints for users to view their session
|
||||
h.Path("/").HandlerFunc(p.userInfo).Methods(http.MethodGet)
|
||||
h.Path("/sign_out").HandlerFunc(p.SignOut).Methods(http.MethodGet, http.MethodPost)
|
||||
h.Path("/jwt").Handler(httputil.HandlerFunc(p.jwtAssertion)).Methods(http.MethodGet)
|
||||
|
||||
// Authenticate service callback handlers and middleware
|
||||
// callback used to set route-scoped session and redirect back to destination
|
||||
// only accept signed requests (hmac) from other trusted pomerium services
|
||||
// called following authenticate auth flow to grab a new or existing session
|
||||
// the route specific cookie is returned in a signed query params
|
||||
c := r.PathPrefix(dashboardPath + "/callback").Subrouter()
|
||||
c.Use(func(h http.Handler) http.Handler {
|
||||
return middleware.ValidateSignature(p.state.Load().sharedKey)(h)
|
||||
|
@ -52,9 +51,9 @@ func (p *Proxy) RobotsTxt(w http.ResponseWriter, _ *http.Request) {
|
|||
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
|
||||
}
|
||||
|
||||
// SignOut redirects the request to the sign out url. It's the responsibility
|
||||
// of the authenticate service to revoke the remote session and clear
|
||||
// the local session state.
|
||||
// SignOut clears the local session and redirects the request to the sign out url.
|
||||
// It's the responsibility of the authenticate service to revoke the remote session and clear
|
||||
// the authenticate service's session state.
|
||||
func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
|
||||
state := p.state.Load()
|
||||
|
||||
|
@ -75,8 +74,7 @@ func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) {
|
|||
httputil.Redirect(w, r, urlutil.NewSignedURL(state.sharedKey, &signoutURL).String(), http.StatusFound)
|
||||
}
|
||||
|
||||
// UserDashboard redirects to the authenticate dashboard.
|
||||
func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
func (p *Proxy) userInfo(w http.ResponseWriter, r *http.Request) {
|
||||
state := p.state.Load()
|
||||
|
||||
redirectURL := urlutil.GetAbsoluteURL(r).String()
|
||||
|
@ -93,7 +91,7 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// Callback handles the result of a successful call to the authenticate service
|
||||
// and is responsible setting returned per-route session.
|
||||
// and is responsible setting per-route sessions.
|
||||
func (p *Proxy) Callback(w http.ResponseWriter, r *http.Request) error {
|
||||
redirectURLString := r.FormValue(urlutil.QueryRedirectURI)
|
||||
encryptedSession := r.FormValue(urlutil.QuerySessionEncrypted)
|
||||
|
@ -118,7 +116,7 @@ func (p *Proxy) Callback(w http.ResponseWriter, r *http.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// saveCallbackSession takes an encrypted per-route session token, and decrypts
|
||||
// saveCallbackSession takes an encrypted per-route session token, decrypts
|
||||
// it using the shared service key, then stores it the local session store.
|
||||
func (p *Proxy) saveCallbackSession(w http.ResponseWriter, r *http.Request, enctoken string) ([]byte, error) {
|
||||
state := p.state.Load()
|
||||
|
@ -165,8 +163,7 @@ func (p *Proxy) ProgrammaticLogin(w http.ResponseWriter, r *http.Request) error
|
|||
return nil
|
||||
}
|
||||
|
||||
// jwtAssertion returns the current user's/request's JWT (rfc7519#section-10.3.1) that should be
|
||||
// added to the upstream request.
|
||||
// jwtAssertion returns the current request's JWT assertion (rfc7519#section-10.3.1).
|
||||
func (p *Proxy) jwtAssertion(w http.ResponseWriter, r *http.Request) error {
|
||||
assertionJWT := r.Header.Get(httputil.HeaderPomeriumJWTAssertion)
|
||||
if assertionJWT == "" {
|
||||
|
|
|
@ -64,7 +64,7 @@ func TestProxy_Signout(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestProxy_UserDashboard(t *testing.T) {
|
||||
func TestProxy_userInfo(t *testing.T) {
|
||||
opts := testOptions(t)
|
||||
err := ValidateOptions(opts)
|
||||
if err != nil {
|
||||
|
@ -76,7 +76,7 @@ func TestProxy_UserDashboard(t *testing.T) {
|
|||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "/.pomerium/sign_out", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
proxy.UserDashboard(rr, req)
|
||||
proxy.userInfo(rr, req)
|
||||
if status := rr.Code; status != http.StatusFound {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusFound)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue