Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auth expire self / logout #1279

Merged
merged 9 commits into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions internal/cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,8 @@ func authenticationHTTPMount(
registerFunc(ctx, conn, rpcauth.RegisterPublicAuthenticationServiceHandler),
registerFunc(ctx, conn, rpcauth.RegisterAuthenticationServiceHandler),
}
middleware = func(next http.Handler) http.Handler {
return next
}
authmiddleware = auth.NewHTTPMiddleware()
middleware = []func(next http.Handler) http.Handler{authmiddleware.Handler}
)

if cfg.Methods.Token.Enabled {
Expand All @@ -136,11 +135,11 @@ func authenticationHTTPMount(
runtime.WithForwardResponseOption(oidcmiddleware.ForwardResponseOption),
registerFunc(ctx, conn, rpcauth.RegisterAuthenticationMethodOIDCServiceHandler))

middleware = oidcmiddleware.Handler
middleware = append(middleware, oidcmiddleware.Handler)
}

r.Group(func(r chi.Router) {
r.Use(middleware)
r.Use(middleware...)

r.Mount("/auth/v1", gateway.NewGatewayServeMux(muxOpts...))
})
Expand Down
42 changes: 42 additions & 0 deletions internal/server/auth/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package auth

import (
"net/http"
)

var (
stateCookieKey = "flipt_client_state"
)

// Middleware contains various extensions for appropriate integration of the generic auth services
// behind gRPC gateway. This currently includes clearing the appropriate cookies on logout.
type Middleware struct{}

// NewHTTPMiddleware constructs a new auth HTTP middleware.
func NewHTTPMiddleware() *Middleware {
return &Middleware{}
}

// Handler is a http middleware used to decorate the auth provider gateway handler.
// This is used to clear the appropriate cookies on logout.
func (m Middleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut || r.URL.Path == "/auth/v1/self/expire" {
next.ServeHTTP(w, r)
return
}

for _, cookieName := range []string{stateCookieKey, tokenCookieKey} {
cookie := &http.Cookie{
Name: cookieName,
markphelps marked this conversation as resolved.
Show resolved Hide resolved
Value: "",
Path: "/",
MaxAge: -1,
}

http.SetCookie(w, cookie)
}

next.ServeHTTP(w, r)
})
}
20 changes: 20 additions & 0 deletions internal/server/auth/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import (
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
)

var _ auth.AuthenticationServiceServer = &Server{}

// Server is the core AuthenticationServiceServer implementations.
//
// It is the service which presents all Authentications created in the backing auth store.
Expand Down Expand Up @@ -82,3 +85,20 @@ func (s *Server) DeleteAuthentication(ctx context.Context, req *auth.DeleteAuthe

return &emptypb.Empty{}, s.store.DeleteAuthentications(ctx, storageauth.Delete(storageauth.WithID(req.Id)))
}

// ExpireAuthenticationSelf expires the Authentication which was derived from the request context.
// If no expire_at is provided, the current time is used. This is useful for logging out a user.
// If the expire_at is greater than the current expiry time, the expiry time is extended.
func (s *Server) ExpireAuthenticationSelf(ctx context.Context, req *auth.ExpireAuthenticationSelfRequest) (*emptypb.Empty, error) {
if auth := GetAuthenticationFrom(ctx); auth != nil {
s.logger.Debug("ExpireAuthentication", zap.String("id", auth.Id))

if req.ExpiresAt == nil || !req.ExpiresAt.IsValid() {
req.ExpiresAt = timestamppb.Now()
}

return &emptypb.Empty{}, s.store.ExpireAuthenticationByID(ctx, auth.Id, req.ExpiresAt)
}

return nil, errUnauthenticated
}
31 changes: 31 additions & 0 deletions internal/server/auth/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,35 @@ func TestServer(t *testing.T) {
var notFound errors.ErrNotFound
require.ErrorAs(t, err, &notFound)
})

t.Run("ExpireAuthenticationSelf", func(t *testing.T) {
// create new authentication
req := &storageauth.CreateAuthenticationRequest{
Method: auth.Method_METHOD_TOKEN,
ExpiresAt: timestamppb.New(time.Now().Add(time.Hour).UTC()),
}

ctx := context.TODO()

clientToken, _, err := store.CreateAuthentication(ctx, req)
require.NoError(t, err)

ctx = metadata.AppendToOutgoingContext(
ctx,
"authorization",
"Bearer "+clientToken,
)

// get self with authenticated context not unauthorized
_, err = client.GetAuthenticationSelf(ctx, &emptypb.Empty{})
require.NoError(t, err)

// expire self
_, err = client.ExpireAuthenticationSelf(ctx, &auth.ExpireAuthenticationSelfRequest{})
require.NoError(t, err)

// get self with authenticated context now unauthorized
_, err = client.GetAuthenticationSelf(ctx, &emptypb.Empty{})
require.ErrorIs(t, err, errUnauthenticated)
})
}
2 changes: 2 additions & 0 deletions internal/storage/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type Store interface {
// Use DeleteByID to construct a request to delete a single Authentication by ID string.
// Use DeleteByMethod to construct a request to delete 0 or more Authentications by Method and optional expired before constraint.
DeleteAuthentications(context.Context, *DeleteAuthenticationsRequest) error
// ExpireAuthenticationByID attempts to expire an Authentication by ID string and the provided expiry time.
ExpireAuthenticationByID(context.Context, string, *timestamppb.Timestamp) error
}

// CreateAuthenticationRequest is the argument passed when creating instances
Expand Down
13 changes: 13 additions & 0 deletions internal/storage/auth/memory/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,16 @@ func (s *Store) DeleteAuthentications(_ context.Context, req *auth.DeleteAuthent

return nil
}

// ExpireAuthenticationByID attempts to expire an Authentication by ID string and the provided expiry time.
func (s *Store) ExpireAuthenticationByID(ctx context.Context, id string, expireAt *timestamppb.Timestamp) error {
s.mu.Lock()
authentication, ok := s.byID[id]
s.mu.Unlock()
if !ok {
return errors.ErrNotFoundf("getting authentication by token")
}

authentication.ExpiresAt = expireAt
return nil
}
13 changes: 13 additions & 0 deletions internal/storage/auth/sql/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,19 @@ func (s *Store) DeleteAuthentications(ctx context.Context, req *storageauth.Dele
return
}

// ExpireAuthenticationByID attempts to expire an Authentication by ID string and the provided expiry time.
func (s *Store) ExpireAuthenticationByID(ctx context.Context, id string, expireAt *timestamppb.Timestamp) (err error) {
defer s.adaptError("expiring authentication by id: %w", &err)

_, err = s.builder.
Update("authentications").
Set("expires_at", &storagesql.Timestamp{Timestamp: expireAt}).
Where(sq.Eq{"id": id}).
ExecContext(ctx)

return
}

func (s *Store) scanAuthentication(scanner sq.RowScanner, authentication *rpcauth.Authentication) error {
var (
expiresAt storagesql.NullableTimestamp
Expand Down
11 changes: 11 additions & 0 deletions internal/storage/auth/testing/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,15 @@ func TestAuthenticationStoreHarness(t *testing.T, fn func(t *testing.T) storagea
fmt.Println("Found:", len(all))
}
})

t.Run("Expire a single instance by ID", func(t *testing.T) {
expiresAt := timestamppb.New(time.Now().UTC().Add(-time.Hour))
// expire the first token
err := store.ExpireAuthenticationByID(ctx, created[0].Auth.Id, expiresAt)
require.NoError(t, err)

auth, err := store.GetAuthenticationByClientToken(ctx, created[0].Token)
require.NoError(t, err)
assert.True(t, auth.ExpiresAt.AsTime().Before(time.Now().UTC()))
})
}
Loading