-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
847 additions
and
239 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
106 changes: 106 additions & 0 deletions
106
internal/pkg/service/appsproxy/proxy/apphandler/authproxy/basicauth/handler.go
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,106 @@ | ||
package basicauth | ||
|
||
import ( | ||
"net/http" | ||
"time" | ||
|
||
"github.com/benbjohnson/clock" | ||
|
||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/config" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/chain" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/pagewriter" | ||
"github.com/keboola/keboola-as-code/internal/pkg/utils/errors" | ||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util" | ||
) | ||
|
||
const ( | ||
basicAuthCookie = "proxyBasicAuth" | ||
formPagePath = config.InternalPrefix + "/form" | ||
) | ||
|
||
type Handler struct { | ||
basicAuth provider.Basic | ||
upstream chain.Handler | ||
app api.AppConfig | ||
pageWriter *pagewriter.Writer | ||
clock clock.Clock | ||
} | ||
|
||
func NewHandler( | ||
auth provider.Basic, | ||
app api.AppConfig, | ||
upstream chain.Handler, | ||
pageWriter *pagewriter.Writer, | ||
clock clock.Clock, | ||
) *Handler { | ||
return &Handler{ | ||
basicAuth: auth, | ||
app: app, | ||
upstream: upstream, | ||
pageWriter: pageWriter, | ||
clock: clock, | ||
} | ||
} | ||
|
||
func (h *Handler) Name() string { | ||
return h.basicAuth.Name() | ||
} | ||
|
||
func (h *Handler) SignInPath() string { | ||
return "/form" | ||
} | ||
|
||
func (h *Handler) CookieExpiration() time.Duration { | ||
return 5 * time.Minute | ||
} | ||
|
||
func (h *Handler) ServeHTTPOrError(w http.ResponseWriter, req *http.Request) error { | ||
cookie, _ := req.Cookie(basicAuthCookie) | ||
if _, ok := req.Form["password"]; !ok || cookie == nil { | ||
h.pageWriter.WriteFormPage(w, req, http.StatusOK) | ||
return nil | ||
} | ||
|
||
if req.URL.Path != "_proxy/form" { | ||
return nil | ||
//return errors.New("404") | ||
} | ||
|
||
// TODO: check post body for form filled value | ||
// Set cookies with `CookieExpiration`. Cookie "proxyBasicAuth": "sha1(value, salt from config)" | ||
// Test cases: 1. Wrong password, Wrong cookie, Correct password, Correct cookie, sign_out with deletion of cookie | ||
if !h.basicAuth.IsAuthorized("") { | ||
// TODO: here should be pagewriter injected? | ||
// TODO: specific error catch on pagewriter | ||
return errors.New("wrong password prompted") | ||
} | ||
|
||
// TODO: check cookie before setting it | ||
host, _ := util.SplitHostPort(req.Host) | ||
if host == "" { | ||
panic(errors.New("host cannot be empty")) | ||
} | ||
|
||
v := &http.Cookie{ | ||
Name: basicAuthCookie, | ||
// Value: value, sha1 | ||
Path: "/", | ||
Domain: host, | ||
Secure: true, | ||
HttpOnly: true, | ||
SameSite: http.SameSiteStrictMode, | ||
} | ||
|
||
expires := h.CookieExpiration() | ||
if expires > 0 { | ||
// If there is an expiration, set it | ||
v.Expires = h.clock.Now().Add(expires) | ||
} else { | ||
// Otherwise clear the cookie | ||
v.MaxAge = -1 | ||
} | ||
|
||
return h.upstream.ServeHTTPOrError(w, req) | ||
} |
62 changes: 62 additions & 0 deletions
62
internal/pkg/service/appsproxy/proxy/apphandler/authproxy/manager.go
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,62 @@ | ||
package authproxy | ||
|
||
import ( | ||
"github.com/benbjohnson/clock" | ||
|
||
"github.com/keboola/keboola-as-code/internal/pkg/log" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/config" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/basicauth" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/selector" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/chain" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/pagewriter" | ||
) | ||
|
||
type Manager struct { | ||
logger log.Logger | ||
config config.Config | ||
pageWriter *pagewriter.Writer | ||
clock clock.Clock | ||
providerSelector *selector.Selector | ||
} | ||
|
||
type dependencies interface { | ||
Logger() log.Logger | ||
Clock() clock.Clock | ||
Config() config.Config | ||
PageWriter() *pagewriter.Writer | ||
} | ||
|
||
func NewManager(d dependencies) *Manager { | ||
return &Manager{ | ||
logger: d.Logger(), | ||
config: d.Config(), | ||
pageWriter: d.PageWriter(), | ||
clock: d.Clock(), | ||
providerSelector: selector.New(d), | ||
} | ||
} | ||
|
||
func (m *Manager) ProviderSelector() *selector.Selector { | ||
return m.providerSelector | ||
} | ||
|
||
func (m *Manager) NewHandlers(app api.AppConfig, upstream chain.Handler) map[provider.ID]selector.Handler { | ||
authHandlers := make(map[provider.ID]selector.Handler, len(app.AuthProviders)) | ||
// TODO: factory method | ||
for _, auth := range app.AuthProviders { | ||
switch p := auth.(type) { | ||
case provider.OIDC: | ||
authHandlers[auth.ID()] = oidcproxy.NewHandler(m.logger, m.config, m.providerSelector, m.pageWriter, app, p, upstream) | ||
|
||
case provider.Basic: | ||
authHandlers[auth.ID()] = basicauth.NewHandler(p, app, upstream, m.pageWriter, m.clock) | ||
|
||
default: | ||
panic("unknown auth provider type") | ||
} | ||
} | ||
return authHandlers | ||
} |
116 changes: 116 additions & 0 deletions
116
internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/config.go
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,116 @@ | ||
package oidcproxy | ||
|
||
import ( | ||
"crypto/sha256" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" | ||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation" | ||
|
||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/config" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/selector" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/chain" | ||
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/pagewriter" | ||
"github.com/keboola/keboola-as-code/internal/pkg/utils/errors" | ||
) | ||
|
||
func proxyConfig( | ||
cfg config.Config, | ||
selector *selector.Selector, | ||
pageWriter *pagewriter.Writer, | ||
app api.AppConfig, | ||
authProvider provider.OIDC, | ||
upstream chain.Handler, | ||
) (*options.Options, error) { | ||
// Generate unique cookies secret | ||
secret, err := generateCookieSecret(cfg, app, authProvider.ID()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
proxyProvider, err := authProvider.ProxyProviderOptions() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
v := options.NewOptions() | ||
|
||
// Connect to the app upstream | ||
v.UpstreamHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||
if err := upstream.ServeHTTPOrError(w, req); err != nil { | ||
pageWriter.WriteError(w, req, &app, err) | ||
} | ||
}) | ||
|
||
// Render the selector page, if login is needed, it is not an internal URL | ||
v.OnNeedsLogin = func(rw http.ResponseWriter, req *http.Request) (stop bool) { | ||
// Bypass internal paths | ||
if strings.HasPrefix(req.URL.Path, v.ProxyPrefix) { | ||
return false | ||
} | ||
|
||
return selector.OnNeedsLogin(&app, pageWriter.WriteError)(rw, req) | ||
} | ||
// Setup | ||
domain := app.CookieDomain(cfg.API.PublicURL) | ||
redirectURL := cfg.API.PublicURL.Scheme + "://" + domain + config.InternalPrefix + "/callback" | ||
v.Logging.RequestIDHeader = config.RequestIDHeader | ||
v.Logging.RequestEnabled = false // we have log middleware for all requests | ||
v.Cookie.Secret = secret | ||
v.Cookie.Domains = []string{domain} | ||
v.Cookie.SameSite = "strict" | ||
v.ProxyPrefix = config.InternalPrefix | ||
v.RawRedirectURL = redirectURL | ||
v.Providers = options.Providers{proxyProvider} | ||
v.SkipProviderButton = true | ||
v.Session = options.SessionOptions{Type: options.CookieSessionStoreType} | ||
v.EmailDomains = []string{"*"} | ||
v.InjectRequestHeaders = []options.Header{ | ||
headerFromClaim("X-Kbc-User-Name", "name"), | ||
headerFromClaim("X-Kbc-User-Email", options.OIDCEmailClaim), | ||
headerFromClaim("X-Kbc-User-Roles", options.OIDCGroupsClaim), | ||
} | ||
|
||
// Cannot separate errors from info because when ErrToInfo is false (default), | ||
// oauthproxy keeps forcibly setting its global error writer to os.Stderr whenever a new proxy instance is created. | ||
v.Logging.ErrToInfo = true | ||
|
||
if err := validation.Validate(v); err != nil { | ||
return nil, err | ||
} | ||
|
||
return v, nil | ||
} | ||
|
||
// generateCookieSecret creates a unique cookie secret for each app and provider. | ||
// This is necessary because otherwise cookies created by provider A would also be valid in a section that requires provider B but not A. | ||
// To solve this we use the combination of the provider id and our salt. | ||
func generateCookieSecret(cfg config.Config, app api.AppConfig, providerID provider.ID) (string, error) { | ||
if cfg.CookieSecretSalt == "" { | ||
return "", errors.New("missing cookie secret salt") | ||
} | ||
|
||
h := sha256.New() | ||
h.Write([]byte(app.ID.String() + "/" + providerID.String() + "/" + cfg.CookieSecretSalt)) | ||
bs := h.Sum(nil) | ||
|
||
// Result must be 32 chars, 2 hex chars for each byte | ||
return fmt.Sprintf("%x", bs[:16]), nil | ||
} | ||
|
||
func headerFromClaim(header, claim string) options.Header { | ||
return options.Header{ | ||
Name: header, | ||
Values: []options.HeaderValue{ | ||
{ | ||
ClaimSource: &options.ClaimSource{ | ||
Claim: claim, | ||
}, | ||
}, | ||
}, | ||
} | ||
} |
Oops, something went wrong.