Skip to content

Commit

Permalink
Auth Logout Handler (flyteorg#42)
Browse files Browse the repository at this point in the history
This adds a logout handler for the auth component.  It will clear the two encrypted cookies used in the oauth flow by setting the expiration to something in the past.

The endpoint also takes the same `redirect_url` query parameter.
To use:
`http://localhost:8088/logout?redirect_url=http://www.google.com`

If there is no redirect url, it just returns a 200 with an empty body.
  • Loading branch information
wild-endeavor authored Dec 13, 2019
1 parent 19f729c commit 8eecb61
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 18 deletions.
1 change: 1 addition & 0 deletions cmd/entrypoints/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ func newHTTPServer(ctx context.Context, cfg *config.ServerConfig, authContext in
// Add HTTP handlers for OAuth2 endpoints
mux.HandleFunc("/login", auth.RefreshTokensIfExists(ctx, authContext,
auth.GetLoginHandler(ctx, authContext)))
mux.HandleFunc("/logout", auth.GetLogoutEndpointHandler(ctx, authContext))
mux.HandleFunc("/callback", auth.GetCallbackHandler(ctx, authContext))
// Install the user info endpoint if there is a user info url configured.
if authContext.GetUserInfoURL() != nil && authContext.GetUserInfoURL().String() != "" {
Expand Down
26 changes: 26 additions & 0 deletions pkg/auth/cookie_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"net/http"
"time"

"github.com/lyft/flytestdlib/errors"
"github.com/lyft/flytestdlib/logger"
Expand Down Expand Up @@ -93,3 +94,28 @@ func (c CookieManager) SetTokenCookies(ctx context.Context, writer http.Response
}
return nil
}

func getLogoutAccessCookie() *http.Cookie {
return &http.Cookie{
Name: accessTokenCookieName,
Value: "",
MaxAge: 0,
HttpOnly: true,
Expires: time.Now().Add(-1 * time.Hour),
}
}

func getLogoutRefreshCookie() *http.Cookie {
return &http.Cookie{
Name: refreshTokenCookieName,
Value: "",
MaxAge: 0,
HttpOnly: true,
Expires: time.Now().Add(-1 * time.Hour),
}
}

func (c CookieManager) DeleteCookies(ctx context.Context, writer http.ResponseWriter) {
http.SetCookie(writer, getLogoutAccessCookie())
http.SetCookie(writer, getLogoutRefreshCookie())
}
29 changes: 29 additions & 0 deletions pkg/auth/cookie_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
Expand Down Expand Up @@ -63,3 +64,31 @@ func TestCookieManager_RetrieveTokenValues(t *testing.T) {
assert.Equal(t, "access", access)
assert.Equal(t, "refresh", refresh)
}

func TestGetLogoutAccessCookie(t *testing.T) {
cookie := getLogoutAccessCookie()
assert.True(t, time.Now().After(cookie.Expires))
}

func TestGetLogoutRefreshCookie(t *testing.T) {
cookie := getLogoutRefreshCookie()
assert.True(t, time.Now().After(cookie.Expires))
}

func TestCookieManager_DeleteCookies(t *testing.T) {
ctx := context.Background()

// These were generated for unit testing only.
hashKeyEncoded := "wG4pE1ccdw/pHZ2ml8wrD5VJkOtLPmBpWbKHmezWXktGaFbRoAhXidWs8OpbA3y7N8vyZhz1B1E37+tShWC7gA" //nolint:goconst
blockKeyEncoded := "afyABVgGOvWJFxVyOvCWCupoTn6BkNl4SOHmahho16Q" //nolint:goconst

manager, err := NewCookieManager(ctx, hashKeyEncoded, blockKeyEncoded)
assert.NoError(t, err)

w := httptest.NewRecorder()
manager.DeleteCookies(ctx, w)
cookies := w.Result().Cookies()
assert.Equal(t, 2, len(cookies))
assert.True(t, time.Now().After(cookies[0].Expires))
assert.True(t, time.Now().After(cookies[1].Expires))
}
32 changes: 17 additions & 15 deletions pkg/auth/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"net/http"

"github.com/gorilla/handlers"
grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
"github.com/lyft/flyteadmin/pkg/auth/interfaces"
"github.com/lyft/flytestdlib/contextutils"
Expand All @@ -20,9 +19,9 @@ import (
)

const (
LoginRedirectURLParameter = "redirect_url"
bearerTokenContextKey contextutils.Key = "bearer"
PrincipalContextKey contextutils.Key = "principal"
RedirectURLParameter = "redirect_url"
bearerTokenContextKey contextutils.Key = "bearer"
PrincipalContextKey contextutils.Key = "principal"
)

type HTTPRequestToMetadataAnnotator func(ctx context.Context, request *http.Request) metadata.MD
Expand Down Expand Up @@ -70,7 +69,7 @@ func GetLoginHandler(ctx context.Context, authContext interfaces.AuthenticationC
logger.Debugf(ctx, "Setting CSRF state cookie to %s and state to %s\n", csrfToken, state)
url := authContext.OAuth2Config().AuthCodeURL(state)
queryParams := request.URL.Query()
if flowEndRedirectURL := queryParams.Get(LoginRedirectURLParameter); flowEndRedirectURL != "" {
if flowEndRedirectURL := queryParams.Get(RedirectURLParameter); flowEndRedirectURL != "" {
redirectCookie := NewRedirectCookie(ctx, flowEndRedirectURL)
if redirectCookie != nil {
http.SetCookie(writer, redirectCookie)
Expand Down Expand Up @@ -252,15 +251,18 @@ func GetMetadataEndpointRedirectHandler(ctx context.Context, authCtx interfaces.
}
}

// These are here for CORS handling. Actual serving of the OPTIONS request will be done by the gorilla/handlers package
type CorsHandlerDecorator func(http.Handler) http.Handler
func GetLogoutEndpointHandler(ctx context.Context, authCtx interfaces.AuthenticationContext) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
logger.Debugf(ctx, "Deleting auth cookies")
authCtx.CookieManager().DeleteCookies(ctx, writer)

// This produces a decorator that, when applied to an existing Handler, will first test if the request is an appropriate
// options request, and if so, serve it. If not, the underlying handler will be called.
func GetCorsDecorator(ctx context.Context, allowedOrigins, allowedHeaders []string) CorsHandlerDecorator {
logger.Debugf(ctx, "Creating CORS decorator with allowed origins %v", allowedOrigins)
return handlers.CORS(handlers.AllowedHeaders(allowedHeaders),
handlers.AllowedMethods([]string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
http.MethodHead, http.MethodOptions, http.MethodDelete}),
handlers.AllowedOrigins(allowedOrigins))
// Redirect if one was given
queryParams := request.URL.Query()
if redirectURL := queryParams.Get(RedirectURLParameter); redirectURL != "" {
http.Redirect(writer, request, redirectURL, http.StatusTemporaryRedirect)
}
}
}

// These are here for CORS handling. Actual serving of the OPTIONS request will be done by the gorilla/handlers package
type CorsHandlerDecorator func(http.Handler) http.Handler
1 change: 1 addition & 0 deletions pkg/auth/interfaces/cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ import (
type CookieHandler interface {
RetrieveTokenValues(ctx context.Context, request *http.Request) (accessToken string, refreshToken string, err error)
SetTokenCookies(ctx context.Context, writer http.ResponseWriter, token *oauth2.Token) error
DeleteCookies(ctx context.Context, writer http.ResponseWriter)
}
6 changes: 3 additions & 3 deletions pkg/rpc/adminservice/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func NewAdminServer(kubeConfig, master string) *AdminService {
defer func() {
if err := recover(); err != nil {
adminScope.MustNewCounter("initialization_panic",
"panics encountered initializating the admin service").Inc()
"panics encountered initializing the admin service").Inc()
logger.Fatalf(context.Background(), fmt.Sprintf("caught panic: %v [%+v]", err, string(debug.Stack())))
}
}()
Expand All @@ -74,14 +74,14 @@ func NewAdminServer(kubeConfig, master string) *AdminService {
db := repositories.GetRepository(
repositories.POSTGRES, dbConfig, adminScope.NewSubScope("database"))
storeConfig := storage.GetConfig()
executionCluster := executionCluster.GetExecutionCluster(
execCluster := executionCluster.GetExecutionCluster(
adminScope.NewSubScope("executor").NewSubScope("cluster"),
kubeConfig,
master,
configuration)
workflowExecutor := workflowengine.NewFlytePropeller(
applicationConfiguration.RoleNameKey,
executionCluster,
execCluster,
adminScope.NewSubScope("executor").NewSubScope("flytepropeller"),
configuration.NamespaceMappingConfiguration())
logger.Info(context.Background(), "Successfully created a workflow executor engine")
Expand Down

0 comments on commit 8eecb61

Please sign in to comment.