Skip to content

Commit

Permalink
Merge pull request #1102 from flipt-io/gm/authentication-grpc
Browse files Browse the repository at this point in the history
feat(authentication): gRPC service definition for AuthenticationMethodTokenService
  • Loading branch information
GeorgeMac committed Nov 3, 2022
2 parents 0dc7ca5 + 081a897 commit 2487ffb
Show file tree
Hide file tree
Showing 22 changed files with 1,157 additions and 169 deletions.
20 changes: 20 additions & 0 deletions cmd/flipt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,19 @@ import (
"go.flipt.io/flipt/internal/config"
"go.flipt.io/flipt/internal/info"
"go.flipt.io/flipt/internal/server"
authtoken "go.flipt.io/flipt/internal/server/auth/method/token"
"go.flipt.io/flipt/internal/server/cache"
"go.flipt.io/flipt/internal/server/cache/memory"
"go.flipt.io/flipt/internal/server/cache/redis"
"go.flipt.io/flipt/internal/storage"
authsql "go.flipt.io/flipt/internal/storage/auth/sql"
"go.flipt.io/flipt/internal/storage/sql"
"go.flipt.io/flipt/internal/storage/sql/mysql"
"go.flipt.io/flipt/internal/storage/sql/postgres"
"go.flipt.io/flipt/internal/storage/sql/sqlite"
"go.flipt.io/flipt/internal/telemetry"
pb "go.flipt.io/flipt/rpc/flipt"
authrpc "go.flipt.io/flipt/rpc/flipt/auth"
"go.flipt.io/flipt/swagger"
"go.flipt.io/flipt/ui"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
Expand Down Expand Up @@ -542,6 +545,16 @@ func run(ctx context.Context, logger *zap.Logger) error {
})

pb.RegisterFliptServer(grpcServer, srv)

// register auth service
if cfg.Authentication.Enabled {
store := authsql.NewStore(driver, sql.BuilderFor(db, driver), logger)
tokenServer := authtoken.NewServer(logger, store)

authrpc.RegisterAuthenticationMethodTokenServiceServer(grpcServer, tokenServer)
logger.Info("authentication server registered")
}

grpc_prometheus.EnableHandlingTimeHistogram()
grpc_prometheus.Register(grpcServer)
reflection.Register(grpcServer)
Expand Down Expand Up @@ -606,6 +619,12 @@ func run(ctx context.Context, logger *zap.Logger) error {
return fmt.Errorf("registering grpc gateway: %w", err)
}

if cfg.Authentication.Enabled {
if err := authrpc.RegisterAuthenticationMethodTokenServiceHandler(ctx, api, conn); err != nil {
return fmt.Errorf("registering auth grpc gateway: %w", err)
}
}

if cfg.Cors.Enabled {
cors := cors.New(cors.Options{
AllowedOrigins: cfg.Cors.AllowedOrigins,
Expand Down Expand Up @@ -637,6 +656,7 @@ func run(ctx context.Context, logger *zap.Logger) error {
r.Use(middleware.Recoverer)
r.Mount("/metrics", promhttp.Handler())
r.Mount("/api/v1", api)
r.Mount("/auth/v1", api)
r.Mount("/debug", middleware.Profiler())

r.Route("/meta", func(r chi.Router) {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/go-sql-driver/mysql v1.6.0
github.com/gofrs/uuid v4.3.0+incompatible
github.com/golang-migrate/migrate/v4 v4.15.2
github.com/google/go-cmp v0.5.9
github.com/google/go-github/v32 v32.1.0
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
Expand Down
18 changes: 18 additions & 0 deletions internal/config/authentication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package config

import "github.com/spf13/viper"

var _ defaulter = (*AuthenticationConfig)(nil)

// AuthenticationConfig configures Flipts authentication mechanisms
type AuthenticationConfig struct {
Enabled bool `json:"enabled,omitempty" mapstructure:"enabled"`
}

func (a *AuthenticationConfig) setDefaults(v *viper.Viper) []string {
v.SetDefault("authentication", map[string]any{
"enabled": false,
})

return nil
}
19 changes: 10 additions & 9 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,16 @@ var decodeHooks = mapstructure.ComposeDecodeHookFunc(
// then this will be called after unmarshalling, such that the function can emit
// any errors derived from the resulting state of the configuration.
type Config struct {
Log LogConfig `json:"log,omitempty" mapstructure:"log"`
UI UIConfig `json:"ui,omitempty" mapstructure:"ui"`
Cors CorsConfig `json:"cors,omitempty" mapstructure:"cors"`
Cache CacheConfig `json:"cache,omitempty" mapstructure:"cache"`
Server ServerConfig `json:"server,omitempty" mapstructure:"server"`
Tracing TracingConfig `json:"tracing,omitempty" mapstructure:"tracing"`
Database DatabaseConfig `json:"database,omitempty" mapstructure:"db"`
Meta MetaConfig `json:"meta,omitempty" mapstructure:"meta"`
Warnings []string `json:"warnings,omitempty"`
Log LogConfig `json:"log,omitempty" mapstructure:"log"`
UI UIConfig `json:"ui,omitempty" mapstructure:"ui"`
Cors CorsConfig `json:"cors,omitempty" mapstructure:"cors"`
Cache CacheConfig `json:"cache,omitempty" mapstructure:"cache"`
Server ServerConfig `json:"server,omitempty" mapstructure:"server"`
Tracing TracingConfig `json:"tracing,omitempty" mapstructure:"tracing"`
Database DatabaseConfig `json:"database,omitempty" mapstructure:"db"`
Meta MetaConfig `json:"meta,omitempty" mapstructure:"meta"`
Authentication AuthenticationConfig `json:"authentication,omitempty" mapstructure:"authentication"`
Warnings []string `json:"warnings,omitempty"`
}

func Load(path string) (*Config, error) {
Expand Down
57 changes: 57 additions & 0 deletions internal/server/auth/method/token/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package token

import (
"context"
"fmt"

"go.flipt.io/flipt/internal/storage"
"go.flipt.io/flipt/rpc/flipt/auth"
"go.uber.org/zap"
)

const (
storageMetadataNameKey = "io.flipt.auth.token.name"
storageMetadataDescriptionKey = "io.flipt.auth.token.description"
)

// Server is an implementation of auth.AuthenticationMethodTokenServiceServer
//
// It is used to create static tokens within the backing AuthenticationStore.
type Server struct {
logger *zap.Logger
store storage.AuthenticationStore
auth.UnimplementedAuthenticationMethodTokenServiceServer
}

// NewServer constructs and configures a new *Server.
func NewServer(logger *zap.Logger, store storage.AuthenticationStore) *Server {
return &Server{
logger: logger,
store: store,
}
}

// CreateToken adapts and delegates the token request to the backing AuthenticationStore.
//
// Implicitly, the Authentication created will be of type auth.Method_TOKEN.
// Name and Description are both stored in Authentication.Metadata.
// Given the token is created successfully, the generate clientToken string is returned.
// Along with the created Authentication, which includes it's identifier and associated timestamps.
func (s *Server) CreateToken(ctx context.Context, req *auth.CreateTokenRequest) (*auth.CreateTokenResponse, error) {
clientToken, authentication, err := s.store.CreateAuthentication(ctx, &storage.CreateAuthenticationRequest{
Method: auth.Method_TOKEN,
ExpiresAt: req.ExpiresAt,
Metadata: map[string]string{
storageMetadataNameKey: req.GetName(),
storageMetadataDescriptionKey: req.GetDescription(),
},
})
if err != nil {
return nil, fmt.Errorf("attempting to create token: %w", err)
}

return &auth.CreateTokenResponse{
ClientToken: clientToken,
Authentication: authentication,
}, nil
}
98 changes: 98 additions & 0 deletions internal/server/auth/method/token/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package token

import (
"context"
"net"
"testing"

"github.com/google/go-cmp/cmp"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.flipt.io/flipt/internal/server"
"go.flipt.io/flipt/internal/storage/auth/memory"
"go.flipt.io/flipt/rpc/flipt/auth"
"go.uber.org/zap/zaptest"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
)

func TestServer(t *testing.T) {
var (
logger = zaptest.NewLogger(t)
store = memory.NewStore()
listener = bufconn.Listen(1024 * 1024)
server = grpc.NewServer(
grpc_middleware.WithUnaryServerChain(
server.ErrorUnaryInterceptor,
),
)
errC = make(chan error)
shutdown = func(t *testing.T) {
t.Helper()

server.Stop()
if err := <-errC; err != nil {
t.Fatal(err)
}
}
)

defer shutdown(t)

auth.RegisterAuthenticationMethodTokenServiceServer(server, NewServer(logger, store))

go func() {
errC <- server.Serve(listener)
}()

var (
ctx = context.Background()
dialer = func(context.Context, string) (net.Conn, error) {
return listener.Dial()
}
)

conn, err := grpc.DialContext(ctx, "", grpc.WithInsecure(), grpc.WithContextDialer(dialer))
require.NoError(t, err)
defer conn.Close()

client := auth.NewAuthenticationMethodTokenServiceClient(conn)

// attempt to create token
resp, err := client.CreateToken(ctx, &auth.CreateTokenRequest{
Name: "access_all_areas",
Description: "Super secret skeleton key",
})
require.NoError(t, err)

// assert auth is as expected
metadata := resp.Authentication.Metadata
assert.Equal(t, "access_all_areas", metadata["io.flipt.auth.token.name"])
assert.Equal(t, "Super secret skeleton key", metadata["io.flipt.auth.token.description"])

// ensure client token can be used on store to fetch authentication
// and that the authentication returned matches the one received
// by the client
retrieved, err := store.GetAuthenticationByClientToken(ctx, resp.ClientToken)
require.NoError(t, err)

// switch to go-cmp here to do the comparisons since assert trips up
// on the unexported sizeCache values.
if diff := cmp.Diff(retrieved, resp.Authentication, protocmp.Transform()); err != nil {
t.Errorf("-exp/+got:\n%s", diff)
}

// attempt to create token with invalid expires at
_, err = client.CreateToken(ctx, &auth.CreateTokenRequest{
Name: "access_all_areas",
Description: "Super secret skeleton key",
// invalid expires at, nanos must be positive
ExpiresAt: &timestamppb.Timestamp{Nanos: -1},
})
require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "attempting to create token: invalid expiry time: nanos:-1"))
}
49 changes: 49 additions & 0 deletions internal/storage/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package auth

import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
)

const decodedTokenLen = 32

// GenerateRandomToken produces a URL safe base64 encoded string of random characters
// the data is sourced from a pseudo-random input stream
func GenerateRandomToken() string {
var token [decodedTokenLen]byte
if _, err := rand.Read(token[:]); err != nil {
panic(err)
}

return base64.URLEncoding.EncodeToString(token[:])
}

// HashClientToken performs a SHA256 sum on the input string
// it returns the result as a URL safe base64 encoded string
func HashClientToken(token string) (string, error) {
// produce SHA256 hash of token
hash := sha256.New()
if _, err := hash.Write([]byte(token)); err != nil {
return "", fmt.Errorf("hashing client token: %w", err)
}

// base64(sha256sum)
var (
data = make([]byte, 0, base64.URLEncoding.EncodedLen(hash.Size()))
buf = bytes.NewBuffer(data)
enc = base64.NewEncoder(base64.URLEncoding, buf)
)

if _, err := enc.Write(hash.Sum(nil)); err != nil {
return "", fmt.Errorf("hashing client token: %w", err)
}

if err := enc.Close(); err != nil {
return "", fmt.Errorf("hashing client token: %w", err)
}

return buf.String(), nil
}
29 changes: 29 additions & 0 deletions internal/storage/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package auth

import (
"encoding/base64"
"testing"

"github.com/stretchr/testify/require"
)

func FuzzHashClientToken(f *testing.F) {
for _, seed := range []string{
"hello, world",
"supersecretstring",
"egGpvIxtdG6tI3OIJjXOrv7xZW3hRMYg/Lt/G6X/UEwC",
} {
f.Add(seed)
}
for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} {
f.Add(string(seed))
}
f.Fuzz(func(t *testing.T, token string) {
hashed, err := HashClientToken(token)
require.NoError(t, err)
require.NotEmpty(t, hashed, "hashed result is empty")

_, err = base64.URLEncoding.DecodeString(hashed)
require.NoError(t, err)
})
}
Loading

0 comments on commit 2487ffb

Please sign in to comment.