-
Notifications
You must be signed in to change notification settings - Fork 200
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1102 from flipt-io/gm/authentication-grpc
feat(authentication): gRPC service definition for AuthenticationMethodTokenService
- Loading branch information
Showing
22 changed files
with
1,157 additions
and
169 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: ×tamppb.Timestamp{Nanos: -1}, | ||
}) | ||
require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "attempting to create token: invalid expiry time: nanos:-1")) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
Oops, something went wrong.