authenticate: validate origin of signout (#1876) (#1881)

* 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:
github-actions[bot] 2021-02-12 08:53:29 -08:00 committed by GitHub
parent 41d0522da1
commit edb3533d87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 174 additions and 182 deletions

View file

@ -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,10 +267,11 @@ 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")
}
httputil.Redirect(w, r, redirectString, http.StatusFound)
return nil
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
@ -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")
}

View file

@ -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)
}
})
}
}

View file

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

View file

@ -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
View file

@ -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
View file

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

View file

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

View file

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

View file

@ -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 == "" {

View file

@ -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)
}