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

fix(auth): allow authorization for Github and OIDC behind the reverse proxy #3135

Merged
merged 1 commit into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions examples/proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Running Flipt behind the reverse proxy with custom path

Use `[email protected]` and `password` for authentication in Dex
29 changes: 29 additions & 0 deletions examples/proxy/dex.config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
issuer: http://localhost:5556/dex

storage:
type: sqlite3
config:
file: /var/dex/dex.db

web:
http: 0.0.0.0:5556

staticClients:
- id: example-app
redirectURIs:
- http://localhost:8080/flipt/auth/v1/method/oidc/dex/callback
name: Example App
secret: ZXhhbXBsZS1hcHAtc2VjcmV0

connectors:
- type: mockCallback
id: mock
name: Example

enablePasswordDB: true

staticPasswords:
- email: "[email protected]"
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" #password
username: "admin"
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
14 changes: 12 additions & 2 deletions examples/proxy/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,23 @@ services:
- flipt_network
volumes:
- ./features.yml:/opt/features.yml:ro
- ./flipt.config.yml:/etc/flipt/config/default.yml:ro
- ./hosts.txt:/etc/hosts:ro
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
test: wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/health || exit 1

localhost:
image: ghcr.io/dexidp/dex:latest-distroless
ports:
- "5556:5556"
networks:
- flipt_network
volumes:
- ./dex.config.yml:/etc/dex/config.docker.yaml
nginx:
image: nginx:alpine
ports:
- "9999:80"
- "8080:80"
networks:
- flipt_network
volumes:
Expand Down
18 changes: 18 additions & 0 deletions examples/proxy/flipt.config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
authentication:
required: true
session:
domain: "localhost:8080"
secure: false
methods:
oidc:
enabled: true
email_matches:
- ^.*@example.com$
providers:
dex:
issuer_url: http://localhost:5556/dex
client_id: example-app
client_secret: ZXhhbXBsZS1hcHAtc2VjcmV0
redirect_address: http://localhost:8080/flipt
scopes:
- email
Empty file added examples/proxy/hosts.txt
Empty file.
1 change: 1 addition & 0 deletions examples/proxy/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Prefix /flipt;
proxy_buffering on;

# rewrite
Expand Down
2 changes: 1 addition & 1 deletion internal/cleanup/cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func NewAuthenticationService(logger *zap.Logger, lock oplock.Service, store aut
func (s *AuthenticationService) Run(ctx context.Context) {
ctx, s.cancel = context.WithCancel(ctx)

for _, info := range s.config.Methods.AllMethods() {
for _, info := range s.config.Methods.AllMethods(ctx) {
logger := s.logger.With(zap.Stringer("method", info.Method))
if info.Cleanup == nil {
if info.Enabled {
Expand Down
4 changes: 2 additions & 2 deletions internal/cleanup/cleanup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestCleanup(t *testing.T) {
)

// enable all methods and set their cleanup configuration
for _, info := range authConfig.Methods.AllMethods() {
for _, info := range authConfig.Methods.AllMethods(ctx) {
info.Enable(t)
info.SetCleanup(t, config.AuthenticationCleanupSchedule{
Interval: time.Second,
Expand Down Expand Up @@ -64,7 +64,7 @@ func TestCleanup(t *testing.T) {
assert.Equal(t, storedAuth, retrievedAuth)
})

for _, info := range authConfig.Methods.AllMethods() {
for _, info := range authConfig.Methods.AllMethods(ctx) {
info := info
if !info.RequiresDatabase {
continue
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ func authenticationHTTPMount(
}

if cfg.SessionEnabled() {
muxOpts = append(muxOpts, runtime.WithMetadata(method.ForwardCookies))
muxOpts = append(muxOpts, runtime.WithMetadata(method.ForwardCookies), runtime.WithMetadata(method.ForwardPrefix))

methodMiddleware := method.NewHTTPMiddleware(cfg.Session)
muxOpts = append(muxOpts, runtime.WithForwardResponseOption(methodMiddleware.ForwardResponseOption))
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"go.flipt.io/flipt/internal/config"
"go.flipt.io/flipt/internal/gateway"
"go.flipt.io/flipt/internal/info"
"go.flipt.io/flipt/internal/server/authn/method"
"go.flipt.io/flipt/rpc/flipt"
"go.flipt.io/flipt/rpc/flipt/analytics"
"go.flipt.io/flipt/rpc/flipt/evaluation"
Expand Down Expand Up @@ -176,6 +177,7 @@ func NewHTTPServer(
conn,
meta.RegisterMetadataServiceHandler,
),
runtime.WithMetadata(method.ForwardPrefix),
))
})
})
Expand Down
75 changes: 47 additions & 28 deletions internal/config/authentication.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package config

import (
"context"
"fmt"
"net/url"
"os"
"path"
"slices"
"strings"
"testing"
Expand Down Expand Up @@ -65,7 +67,7 @@ func (c AuthenticationConfig) Enabled() bool {
return true
}

for _, info := range c.Methods.AllMethods() {
for _, info := range c.Methods.AllMethods(context.Background()) {
if info.Enabled {
return true
}
Expand All @@ -77,7 +79,7 @@ func (c AuthenticationConfig) Enabled() bool {
// RequiresDatabase returns true if any of the enabled authentication
// methods requires a database connection
func (c AuthenticationConfig) RequiresDatabase() bool {
for _, info := range c.Methods.AllMethods() {
for _, info := range c.Methods.AllMethods(context.Background()) {
if info.Enabled && info.RequiresDatabase {
return true
}
Expand All @@ -96,7 +98,7 @@ func (c AuthenticationConfig) IsZero() bool {
// It returns true given at-least 1 method is enabled and it's associated schedule
// has been configured (non-nil).
func (c AuthenticationConfig) ShouldRunCleanup() (shouldCleanup bool) {
for _, info := range c.Methods.AllMethods() {
for _, info := range c.Methods.AllMethods(context.Background()) {
shouldCleanup = shouldCleanup || info.RequiresCleanup()
}

Expand All @@ -107,7 +109,7 @@ func (c *AuthenticationConfig) setDefaults(v *viper.Viper) error {
methods := map[string]any{}

// set default for each methods
for _, info := range c.Methods.AllMethods() {
for _, info := range c.Methods.AllMethods(context.Background()) {
method := map[string]any{"enabled": false}
// if the method has been enabled then set the defaults
// for its cleanup strategy
Expand Down Expand Up @@ -139,7 +141,7 @@ func (c *AuthenticationConfig) setDefaults(v *viper.Viper) error {

func (c *AuthenticationConfig) SessionEnabled() bool {
var sessionEnabled bool
for _, info := range c.Methods.AllMethods() {
for _, info := range c.Methods.AllMethods(context.Background()) {
sessionEnabled = sessionEnabled || (info.Enabled && info.SessionCompatible)
}

Expand All @@ -149,7 +151,7 @@ func (c *AuthenticationConfig) SessionEnabled() bool {
func (c *AuthenticationConfig) validate() error {
var sessionEnabled bool

for _, info := range c.Methods.AllMethods() {
for _, info := range c.Methods.AllMethods(context.Background()) {
if !info.RequiresCleanup() {
continue
}
Expand Down Expand Up @@ -189,7 +191,7 @@ func (c *AuthenticationConfig) validate() error {
c.Session.Domain = host
}

for _, info := range c.Methods.AllMethods() {
for _, info := range c.Methods.AllMethods(context.Background()) {
if err := info.validate(); err != nil {
return err
}
Expand Down Expand Up @@ -251,21 +253,32 @@ type AuthenticationMethods struct {
}

// AllMethods returns all the AuthenticationMethod instances available.
func (a *AuthenticationMethods) AllMethods() []StaticAuthenticationMethodInfo {
func (a *AuthenticationMethods) AllMethods(ctx context.Context) []StaticAuthenticationMethodInfo {
return []StaticAuthenticationMethodInfo{
a.Token.info(),
a.Github.info(),
a.OIDC.info(),
a.Kubernetes.info(),
a.JWT.info(),
a.Cloud.info(),
a.Token.info(ctx),
a.Github.info(ctx),
a.OIDC.info(ctx),
a.Kubernetes.info(ctx),
a.JWT.info(ctx),
a.Cloud.info(ctx),
}
}

type forwardPrefixContext struct{}

func WithForwardPrefix(ctx context.Context, prefix string) context.Context {
return context.WithValue(ctx, forwardPrefixContext{}, prefix)
}

func getForwardPrefix(ctx context.Context) string {
prefix, _ := ctx.Value(forwardPrefixContext{}).(string)
return prefix
}

// EnabledMethods returns all the AuthenticationMethod instances that have been enabled.
func (a *AuthenticationMethods) EnabledMethods() []StaticAuthenticationMethodInfo {
var enabled []StaticAuthenticationMethodInfo
for _, info := range a.AllMethods() {
for _, info := range a.AllMethods(context.Background()) {
if info.Enabled {
enabled = append(enabled, info)
}
Expand Down Expand Up @@ -329,7 +342,7 @@ func (a AuthenticationMethodInfo) Name() string {
// methods properties.
type AuthenticationMethodInfoProvider interface {
setDefaults(map[string]any)
info() AuthenticationMethodInfo
info(context.Context) AuthenticationMethodInfo
validate() error
}

Expand All @@ -349,9 +362,9 @@ func (a *AuthenticationMethod[C]) setDefaults(defaults map[string]any) {
a.Method.setDefaults(defaults)
}

func (a *AuthenticationMethod[C]) info() StaticAuthenticationMethodInfo {
func (a *AuthenticationMethod[C]) info(ctx context.Context) StaticAuthenticationMethodInfo {
return StaticAuthenticationMethodInfo{
AuthenticationMethodInfo: a.Method.info(),
AuthenticationMethodInfo: a.Method.info(ctx),
Enabled: a.Enabled,
Cleanup: a.Cleanup,

Expand Down Expand Up @@ -379,7 +392,7 @@ type AuthenticationMethodCloudConfig struct{}
func (a AuthenticationMethodCloudConfig) setDefaults(map[string]any) {}

// info describes properties of the authentication method "cloud".
func (a AuthenticationMethodCloudConfig) info() AuthenticationMethodInfo {
func (a AuthenticationMethodCloudConfig) info(_ context.Context) AuthenticationMethodInfo {
return AuthenticationMethodInfo{
Method: auth.Method_METHOD_CLOUD,
SessionCompatible: true,
Expand All @@ -400,7 +413,7 @@ type AuthenticationMethodTokenConfig struct {
func (a AuthenticationMethodTokenConfig) setDefaults(map[string]any) {}

// info describes properties of the authentication method "token".
func (a AuthenticationMethodTokenConfig) info() AuthenticationMethodInfo {
func (a AuthenticationMethodTokenConfig) info(_ context.Context) AuthenticationMethodInfo {
return AuthenticationMethodInfo{
Method: auth.Method_METHOD_TOKEN,
SessionCompatible: false,
Expand All @@ -427,7 +440,7 @@ type AuthenticationMethodOIDCConfig struct {
func (a AuthenticationMethodOIDCConfig) setDefaults(map[string]any) {}

// info describes properties of the authentication method "oidc".
func (a AuthenticationMethodOIDCConfig) info() AuthenticationMethodInfo {
func (a AuthenticationMethodOIDCConfig) info(ctx context.Context) AuthenticationMethodInfo {
info := AuthenticationMethodInfo{
Method: auth.Method_METHOD_OIDC,
SessionCompatible: true,
Expand All @@ -443,8 +456,14 @@ func (a AuthenticationMethodOIDCConfig) info() AuthenticationMethodInfo {
// to the UI via the /auth/v1/method endpoint
for provider := range a.Providers {
providers[provider] = map[string]any{
"authorize_url": fmt.Sprintf("/auth/v1/method/oidc/%s/authorize", provider),
"callback_url": fmt.Sprintf("/auth/v1/method/oidc/%s/callback", provider),
"authorize_url": path.Join(
getForwardPrefix(ctx),
fmt.Sprintf("/auth/v1/method/oidc/%s/authorize", provider),
),
"callback_url": path.Join(
getForwardPrefix(ctx),
fmt.Sprintf("/auth/v1/method/oidc/%s/callback", provider),
),
}
}

Expand Down Expand Up @@ -520,7 +539,7 @@ func (a AuthenticationMethodKubernetesConfig) setDefaults(defaults map[string]an
}

// info describes properties of the authentication method "kubernetes".
func (a AuthenticationMethodKubernetesConfig) info() AuthenticationMethodInfo {
func (a AuthenticationMethodKubernetesConfig) info(_ context.Context) AuthenticationMethodInfo {
return AuthenticationMethodInfo{
Method: auth.Method_METHOD_KUBERNETES,
SessionCompatible: false,
Expand All @@ -546,7 +565,7 @@ type AuthenticationMethodGithubConfig struct {
func (a AuthenticationMethodGithubConfig) setDefaults(defaults map[string]any) {}

// info describes properties of the authentication method "github".
func (a AuthenticationMethodGithubConfig) info() AuthenticationMethodInfo {
func (a AuthenticationMethodGithubConfig) info(ctx context.Context) AuthenticationMethodInfo {
info := AuthenticationMethodInfo{
Method: auth.Method_METHOD_GITHUB,
SessionCompatible: true,
Expand All @@ -555,8 +574,8 @@ func (a AuthenticationMethodGithubConfig) info() AuthenticationMethodInfo {

metadata := make(map[string]any)

metadata["authorize_url"] = "/auth/v1/method/github/authorize"
metadata["callback_url"] = "/auth/v1/method/github/callback"
metadata["authorize_url"] = path.Join(getForwardPrefix(ctx), "/auth/v1/method/github/authorize")
metadata["callback_url"] = path.Join(getForwardPrefix(ctx), "/auth/v1/method/github/callback")

info.Metadata, _ = structpb.NewStruct(metadata)

Expand Down Expand Up @@ -620,7 +639,7 @@ type AuthenticationMethodJWTConfig struct {
func (a AuthenticationMethodJWTConfig) setDefaults(map[string]any) {}

// info describes properties of the authentication method "jwt".
func (a AuthenticationMethodJWTConfig) info() AuthenticationMethodInfo {
func (a AuthenticationMethodJWTConfig) info(_ context.Context) AuthenticationMethodInfo {
return AuthenticationMethodInfo{
Method: auth.Method_METHOD_JWT,
SessionCompatible: false,
Expand Down
Loading
Loading