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(authentication): gRPC service definition for AuthenticationMethodTokenService #1102

Merged
merged 10 commits into from
Nov 3, 2022
Merged
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 @@ -541,6 +544,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 @@ -605,6 +618,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 @@ -636,6 +655,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 @@ -646,6 +646,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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 AuthenticaitonStore.
GeorgeMac marked this conversation as resolved.
Show resolved Hide resolved
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 Authenticaiton.Metadata.
GeorgeMac marked this conversation as resolved.
Show resolved Hide resolved
// 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