Skip to content

Commit

Permalink
feat(auth): public list authentication methods endpoint (#1240)
Browse files Browse the repository at this point in the history
* refactor(config/auth): add generic authentication method struct

* feat(auth): public list authentication methods endpoint

* test(auth): ensure list auth methods and token cookie

* fix(test): correct URL /auth/v1/method

* fix(test): configure session domain property
  • Loading branch information
GeorgeMac committed Dec 22, 2022
1 parent 5cf2c5c commit 9fe5a88
Show file tree
Hide file tree
Showing 17 changed files with 975 additions and 378 deletions.
2 changes: 1 addition & 1 deletion internal/cleanup/cleanup_test.go
Expand Up @@ -24,7 +24,7 @@ func TestCleanup(t *testing.T) {
lock = inmemoplock.New()
authConfig = config.AuthenticationConfig{
Methods: config.AuthenticationMethods{
Token: config.AuthenticationMethodTokenConfig{
Token: config.AuthenticationMethod[config.AuthenticationMethodTokenConfig]{
Enabled: true,
Cleanup: &config.AuthenticationCleanupSchedule{
Interval: time.Second,
Expand Down
54 changes: 27 additions & 27 deletions internal/cmd/auth.go
Expand Up @@ -14,6 +14,7 @@ import (
"go.flipt.io/flipt/internal/server/auth"
authoidc "go.flipt.io/flipt/internal/server/auth/method/oidc"
authtoken "go.flipt.io/flipt/internal/server/auth/method/token"
"go.flipt.io/flipt/internal/server/auth/public"
storageauth "go.flipt.io/flipt/internal/storage/auth"
storageoplock "go.flipt.io/flipt/internal/storage/oplock"
rpcauth "go.flipt.io/flipt/rpc/flipt/auth"
Expand All @@ -29,9 +30,14 @@ func authenticationGRPC(
oplock storageoplock.Service,
) (grpcRegisterers, []grpc.UnaryServerInterceptor, func(context.Context) error, error) {
var (
public = public.NewServer(logger, cfg)
register = grpcRegisterers{
public,
auth.NewServer(logger, store),
}
authOpts = []containers.Option[auth.InterceptorOptions]{
auth.WithServerSkipsAuthentication(public),
}
interceptors []grpc.UnaryServerInterceptor
shutdown = func(context.Context) error {
return nil
Expand All @@ -55,7 +61,6 @@ func authenticationGRPC(
logger.Debug("authentication method \"token\" server registered")
}

var authOpts []containers.Option[auth.InterceptorOptions]
// register auth method oidc service
if cfg.Methods.OIDC.Enabled {
oidcServer := authoidc.NewServer(logger, store, cfg)
Expand Down Expand Up @@ -96,52 +101,47 @@ func authenticationGRPC(
return register, interceptors, shutdown, nil
}

func registerFunc(ctx context.Context, conn *grpc.ClientConn, fn func(context.Context, *runtime.ServeMux, *grpc.ClientConn) error) runtime.ServeMuxOption {
return func(mux *runtime.ServeMux) {
if err := fn(ctx, mux, conn); err != nil {
panic(err)
}
}
}

func authenticationHTTPMount(
ctx context.Context,
cfg config.AuthenticationConfig,
r chi.Router,
conn *grpc.ClientConn,
) error {
) {
var (
muxOpts []runtime.ServeMuxOption
muxOpts = []runtime.ServeMuxOption{
registerFunc(ctx, conn, rpcauth.RegisterPublicAuthenticationServiceHandler),
registerFunc(ctx, conn, rpcauth.RegisterAuthenticationServiceHandler),
}
middleware = func(next http.Handler) http.Handler {
return next
}
)

// register OIDC middleware if method is enabled
if cfg.Methods.Token.Enabled {
muxOpts = append(muxOpts, registerFunc(ctx, conn, rpcauth.RegisterAuthenticationMethodTokenServiceHandler))
}

if cfg.Methods.OIDC.Enabled {
oidcmiddleware := authoidc.NewHTTPMiddleware(cfg.Session)

muxOpts = append(muxOpts,
runtime.WithMetadata(authoidc.ForwardCookies),
runtime.WithForwardResponseOption(oidcmiddleware.ForwardResponseOption))
middleware = oidcmiddleware.Handler
}
runtime.WithForwardResponseOption(oidcmiddleware.ForwardResponseOption),
registerFunc(ctx, conn, rpcauth.RegisterAuthenticationMethodOIDCServiceHandler))

mux := gateway.NewGatewayServeMux(muxOpts...)

if err := rpcauth.RegisterAuthenticationServiceHandler(ctx, mux, conn); err != nil {
return fmt.Errorf("registering auth grpc gateway: %w", err)
}

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

if cfg.Methods.OIDC.Enabled {
if err := rpcauth.RegisterAuthenticationMethodOIDCServiceHandler(ctx, mux, conn); err != nil {
return fmt.Errorf("registering auth grpc gateway: %w", err)
}
middleware = oidcmiddleware.Handler
}

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

r.Mount("/auth/v1", mux)
r.Mount("/auth/v1", gateway.NewGatewayServeMux(muxOpts...))
})

return nil
}
6 changes: 3 additions & 3 deletions internal/cmd/http.go
Expand Up @@ -98,9 +98,9 @@ func NewHTTPServer(
r.Mount("/metrics", promhttp.Handler())
r.Mount("/api/v1", api)

if err := authenticationHTTPMount(ctx, cfg.Authentication, r, conn); err != nil {
return nil, err
}
// mount all authentication related HTTP components
// to the chi router.
authenticationHTTPMount(ctx, cfg.Authentication, r, conn)

r.Route("/meta", func(r chi.Router) {
r.Use(middleware.SetHeader("Content-Type", "application/json"))
Expand Down
135 changes: 99 additions & 36 deletions internal/config/authentication.go
Expand Up @@ -15,16 +15,20 @@ var (
)

func init() {
for method, v := range auth.Method_value {
if auth.Method(v) == auth.Method_METHOD_NONE {
for _, v := range auth.Method_value {
method := auth.Method(v)
if method == auth.Method_METHOD_NONE {
continue
}

name := strings.ToLower(strings.TrimPrefix(method, "METHOD_"))
stringToAuthMethod[name] = auth.Method(v)
stringToAuthMethod[methodName(method)] = method
}
}

func methodName(method auth.Method) string {
return strings.ToLower(strings.TrimPrefix(auth.Method_name[int32(method)], "METHOD_"))
}

// AuthenticationConfig configures Flipts authentication mechanisms
type AuthenticationConfig struct {
// Required designates whether authentication credentials are validated.
Expand All @@ -39,31 +43,31 @@ type AuthenticationConfig struct {
// ShouldRunCleanup returns true if the cleanup background process should be started.
// It returns true given at-least 1 method is enabled and it's associated schedule
// has been configured (non-nil).
func (c AuthenticationConfig) ShouldRunCleanup() bool {
return (c.Methods.Token.Enabled && c.Methods.Token.Cleanup != nil) ||
(c.Methods.OIDC.Enabled && c.Methods.OIDC.Cleanup != nil)
func (c AuthenticationConfig) ShouldRunCleanup() (shouldCleanup bool) {
for _, info := range c.Methods.AllMethods() {
shouldCleanup = shouldCleanup || (info.Enabled && info.Cleanup != nil)
}

return
}

func (c *AuthenticationConfig) setDefaults(v *viper.Viper) {
methods := map[string]any{
"token": nil,
"oidc": nil,
}
methods := map[string]any{}

// set default for each methods
for k := range methods {
for _, info := range c.Methods.AllMethods() {
method := map[string]any{"enabled": false}
// if the method has been enabled then set the defaults
// for its cleanup strategy
prefix := fmt.Sprintf("authentication.methods.%s", k)
prefix := fmt.Sprintf("authentication.methods.%s", info.Name())
if v.GetBool(prefix + ".enabled") {
method["cleanup"] = map[string]any{
"interval": time.Hour,
"grace_period": 30 * time.Minute,
}
}

methods[k] = method
methods[info.Name()] = method
}

v.SetDefault("authentication", map[string]any{
Expand All @@ -77,32 +81,27 @@ func (c *AuthenticationConfig) setDefaults(v *viper.Viper) {
}

func (c *AuthenticationConfig) validate() error {
for _, cleanup := range []struct {
name string
schedule *AuthenticationCleanupSchedule
}{
// add additional schedules as token methods are created
{"token", c.Methods.Token.Cleanup},
{"oidc", c.Methods.OIDC.Cleanup},
} {
if cleanup.schedule == nil {
var sessionEnabled bool
for _, info := range c.Methods.AllMethods() {
sessionEnabled = sessionEnabled || (info.Enabled && info.SessionCompatible)
if info.Cleanup == nil {
continue
}

field := "authentication.method" + cleanup.name
if cleanup.schedule.Interval <= 0 {
field := "authentication.method" + info.Name()
if info.Cleanup.Interval <= 0 {
return errFieldWrap(field+".cleanup.interval", errPositiveNonZeroDuration)
}

if cleanup.schedule.GracePeriod <= 0 {
if info.Cleanup.GracePeriod <= 0 {
return errFieldWrap(field+".cleanup.grace_period", errPositiveNonZeroDuration)
}
}

// ensure that when a session compatible authentication method has been
// enabled that the session cookie domain has been configured with a non
// empty value.
if c.Methods.OIDC.Enabled {
if sessionEnabled {
if c.Session.Domain == "" {
err := errFieldWrap("authentication.session.domain", errValidationRequired)
return fmt.Errorf("when session compatible auth method enabled: %w", err)
Expand All @@ -129,27 +128,91 @@ type AuthenticationSession struct {
// AuthenticationMethods is a set of configuration for each authentication
// method available for use within Flipt.
type AuthenticationMethods struct {
Token AuthenticationMethodTokenConfig `json:"token,omitempty" mapstructure:"token"`
OIDC AuthenticationMethodOIDCConfig `json:"oidc,omitempty" mapstructure:"oidc"`
Token AuthenticationMethod[AuthenticationMethodTokenConfig] `json:"token,omitempty" mapstructure:"token"`
OIDC AuthenticationMethod[AuthenticationMethodOIDCConfig] `json:"oidc,omitempty" mapstructure:"oidc"`
}

// AllMethods returns all the AuthenticationMethod instances available.
func (a AuthenticationMethods) AllMethods() []StaticAuthenticationMethodInfo {
return []StaticAuthenticationMethodInfo{
a.Token.Info(),
a.OIDC.Info(),
}
}

// StaticAuthenticationMethodInfo embeds an AuthenticationMethodInfo alongside
// the other properties of an AuthenticationMethod.
type StaticAuthenticationMethodInfo struct {
AuthenticationMethodInfo
Enabled bool
Cleanup *AuthenticationCleanupSchedule
}

// AuthenticationMethodInfo is a structure which describes properties
// of a particular authentication method.
// i.e. the name and whether or not the method is session compatible.
type AuthenticationMethodInfo struct {
Method auth.Method
SessionCompatible bool
}

// Name returns the friendly lower-case name for the authentication method.
func (a AuthenticationMethodInfo) Name() string {
return methodName(a.Method)
}

// AuthenticationMethodInfoProvider is a type with a single method Info
// which returns an AuthenticationMethodInfo describing the underlying
// methods properties.
type AuthenticationMethodInfoProvider interface {
Info() AuthenticationMethodInfo
}

// AuthenticationMethod is a container for authentication methods.
// It describes the common properties of all authentication methods.
// Along with leaving a generic slot for the particular method to declare
// its own structural fields. This generic field (Method) must implement
// the AuthenticationMethodInfoProvider to be valid at compile time.
type AuthenticationMethod[C AuthenticationMethodInfoProvider] struct {
Method C `mapstructure:",squash"`
Enabled bool `json:"enabled,omitempty" mapstructure:"enabled"`
Cleanup *AuthenticationCleanupSchedule `json:"cleanup,omitempty" mapstructure:"cleanup"`
}

func (a AuthenticationMethod[C]) Info() StaticAuthenticationMethodInfo {
return StaticAuthenticationMethodInfo{
AuthenticationMethodInfo: a.Method.Info(),
Enabled: a.Enabled,
Cleanup: a.Cleanup,
}
}

// AuthenticationMethodTokenConfig contains fields used to configure the authentication
// method "token".
// This authentication method supports the ability to create static tokens via the
// /auth/v1/method/token prefix of endpoints.
type AuthenticationMethodTokenConfig struct {
// Enabled designates whether or not static token authentication is enabled
// and whether Flipt will mount the "token" method APIs.
Enabled bool `json:"enabled,omitempty" mapstructure:"enabled"`
Cleanup *AuthenticationCleanupSchedule `json:"cleanup,omitempty" mapstructure:"cleanup"`
type AuthenticationMethodTokenConfig struct{}

// Info describes properties of the authentication method "token".
func (a AuthenticationMethodTokenConfig) Info() AuthenticationMethodInfo {
return AuthenticationMethodInfo{
Method: auth.Method_METHOD_TOKEN,
SessionCompatible: false,
}
}

// AuthenticationMethodOIDCConfig configures the OIDC authentication method.
// This method can be used to establish browser based sessions.
type AuthenticationMethodOIDCConfig struct {
Enabled bool `json:"enabled,omitempty" mapstructure:"enabled"`
Providers map[string]AuthenticationMethodOIDCProvider `json:"providers,omitempty" mapstructure:"providers"`
Cleanup *AuthenticationCleanupSchedule `json:"cleanup,omitempty" mapstructure:"cleanup"`
}

// Info describes properties of the authentication method "oidc".
func (a AuthenticationMethodOIDCConfig) Info() AuthenticationMethodInfo {
return AuthenticationMethodInfo{
Method: auth.Method_METHOD_OIDC,
SessionCompatible: true,
}
}

// AuthenticationOIDCProvider configures provider credentials
Expand Down
24 changes: 18 additions & 6 deletions internal/config/config.go
Expand Up @@ -104,8 +104,12 @@ func Load(path string) (*Result, error) {
// search for all expected env vars since Viper cannot
// infer when doing Unmarshal + AutomaticEnv.
// see: https://github.com/spf13/viper/issues/761
structField := val.Type().Field(i)
bindEnvVars(v, getFliptEnvs(), []string{fieldKey(structField)}, structField.Type)
var (
structField = val.Type().Field(i)
key = fieldKey(structField)
)

bindEnvVars(v, getFliptEnvs(), []string{key}, structField.Type)

field := val.Field(i).Addr().Interface()
f(field)
Expand Down Expand Up @@ -150,9 +154,15 @@ type deprecator interface {
deprecations(v *viper.Viper) []deprecation
}

// fieldKey returns the name to be used when deriving a fields env var key.
// If marked as squash the key will be the empty string.
// Otherwise, it is derived from the lowercase name of the field.
func fieldKey(field reflect.StructField) string {
if tag := field.Tag.Get("mapstructure"); tag != "" {
return tag
tag, attr, ok := strings.Cut(tag, ",")
if !ok || attr == "squash" {
return tag
}
}

return strings.ToLower(field.Name)
Expand All @@ -174,14 +184,16 @@ func bindEnvVars(v envBinder, env, prefixes []string, typ reflect.Type) {
case reflect.Map:
// recurse into bindEnvVars while signifying that the last
// key was unbound using the wildcard "*".
bindEnvVars(v, env, append(prefixes, "*"), typ.Elem())
bindEnvVars(v, env, append(prefixes, wildcard), typ.Elem())

return
case reflect.Struct:
for i := 0; i < typ.NumField(); i++ {
structField := typ.Field(i)
var (
structField = typ.Field(i)
key = fieldKey(structField)
)

key := fieldKey(structField)
bind(env, prefixes, key, func(prefixes []string) {
bindEnvVars(v, env, prefixes, structField.Type)
})
Expand Down

0 comments on commit 9fe5a88

Please sign in to comment.