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: Implement basic auth provider structure #1864

Merged
merged 15 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions internal/pkg/service/appsproxy/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Config struct {
CookieSecretSalt string `configKey:"cookieSecretSalt" configUsage:"Cookie secret needed by OAuth 2 Proxy." validate:"required" sensitive:"true"`
Upstream Upstream `configKey:"-" configUsage:"Configuration options for upstream"`
SandboxesAPI SandboxesAPI `configKey:"sandboxesAPI"`
CsrfTokenSalt string `configKey:"csrfTokenSalt" configUsage:"Salt used for generating CSRF tokens" validate:"required" sensitive:"true"`
}

type API struct {
Expand Down
3 changes: 3 additions & 0 deletions internal/pkg/service/appsproxy/dependencies/mocked.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ func NewMockedServiceScope(t *testing.T, cfg config.Config, opts ...dependencies
if cfg.CookieSecretSalt == "" {
cfg.CookieSecretSalt = "foo"
}
if cfg.CsrfTokenSalt == "" {
cfg.CsrfTokenSalt = "bar"
}
if cfg.SandboxesAPI.URL == "" {
cfg.SandboxesAPI.URL = "http://sandboxes-service-api.default.svc.cluster.local"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package basicauth
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"html/template"
"net/http"
"net/url"
"time"

"github.com/benbjohnson/clock"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
"golang.org/x/net/xsrftoken"

"github.com/keboola/keboola-as-code/internal/pkg/log"
"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/config"
Expand All @@ -24,35 +27,39 @@ const (
basicAuthCookie = "proxyBasicAuth"
callbackQueryParam = "rd" // value match OAuth2Proxy internals and shouldn't be modified (see AppDirector there)
formPagePath = config.InternalPrefix + "/form"
csrfTokenKey = "_csrf"
csrfTokenMaxAge = 15 * time.Minute
)

type Handler struct {
logger log.Logger
pageWriter *pagewriter.Writer
clock clock.Clock
publicURL *url.URL
app api.AppConfig
basicAuth provider.Basic
upstream chain.Handler
logger log.Logger
clock clock.Clock
pageWriter *pagewriter.Writer
csrfTokenSalt string
publicURL *url.URL
app api.AppConfig
basicAuth provider.Basic
upstream chain.Handler
}

func NewHandler(
logger log.Logger,
pageWriter *pagewriter.Writer,
config config.Config,
clock clock.Clock,
publicURL *url.URL,
pageWriter *pagewriter.Writer,
app api.AppConfig,
auth provider.Basic,
upstream chain.Handler,
) *Handler {
return &Handler{
logger: logger,
pageWriter: pageWriter,
clock: clock,
publicURL: publicURL,
app: app,
basicAuth: auth,
upstream: upstream,
logger: logger,
clock: clock,
pageWriter: pageWriter,
csrfTokenSalt: config.CsrfTokenSalt,
publicURL: config.API.PublicURL,
app: app,
basicAuth: auth,
upstream: upstream,
}
}

Expand Down Expand Up @@ -91,17 +98,25 @@ func (h *Handler) ServeHTTPOrError(w http.ResponseWriter, req *http.Request) err
return err
}

// Login page
// CSRF token validation
if key := req.Form.Get(csrfTokenKey); key != "" && !xsrftoken.ValidFor(key, csrfTokenKey, h.csrfTokenSalt, "/", csrfTokenMaxAge) {
h.pageWriter.WriteErrorPage(w, req, &h.app, http.StatusForbidden, "Session expired, reload page", pagewriter.ExceptionIDPrefix+key)
return nil
}

// Login page first access
csrfToken := xsrftoken.Generate(csrfTokenKey, h.csrfTokenSalt, "/")
fragment := fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, template.HTMLEscapeString(csrfTokenKey), template.HTMLEscapeString(csrfToken))
if !req.Form.Has("password") && requestCookie == nil {
h.pageWriter.WriteLoginPage(w, req, &h.app, nil)
h.pageWriter.WriteLoginPage(w, req, &h.app, template.HTML(fragment), nil) // #nosec G203
return nil
}

p := req.Form.Get("password")
// Login page with unauthorized alert
p := req.Form.Get("password")
if err := h.isAuthorized(p, requestCookie); err != nil {
h.logger.Warn(req.Context(), err.Error())
h.pageWriter.WriteLoginPage(w, req, &h.app, err)
h.pageWriter.WriteLoginPage(w, req, &h.app, template.HTML(fragment), err) // #nosec G203
return nil
}

Expand All @@ -123,7 +138,7 @@ func (h *Handler) ServeHTTPOrError(w http.ResponseWriter, req *http.Request) err
Value: hex.EncodeToString(hashedValue),
}
h.setCookie(w, host, h.CookieExpiration(), v)
// Redirect to upstream
// Redirect to upstream (Same handler)
w.Header().Set("Location", redirectURL.String())
w.WriteHeader(http.StatusMovedPermanently)
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (m *Manager) NewHandlers(app api.AppConfig, upstream chain.Handler) map[pro
authHandlers[auth.ID()] = oidcproxy.NewHandler(m.logger, m.config, m.providerSelector, m.pageWriter, app, p, upstream)

case provider.Basic:
authHandlers[auth.ID()] = basicauth.NewHandler(m.logger, m.pageWriter, m.clock, m.config.API.PublicURL, app, p, upstream)
authHandlers[auth.ID()] = basicauth.NewHandler(m.logger, m.config, m.clock, m.pageWriter, app, p, upstream)

default:
panic("unknown auth provider type")
Expand Down
10 changes: 6 additions & 4 deletions internal/pkg/service/appsproxy/proxy/pagewriter/login.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package pagewriter

import (
"html/template"
"net/http"

"github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api"
)

type LoginPageData struct {
App AppData
Error error
App AppData
CsrfField template.HTML
Error error
}

func (pw *Writer) WriteLoginPage(w http.ResponseWriter, req *http.Request, app *api.AppConfig, err error) {
func (pw *Writer) WriteLoginPage(w http.ResponseWriter, req *http.Request, app *api.AppConfig, csrfField template.HTML, err error) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate;")
w.Header().Set("pragma", "no-cache")
pw.writePage(w, req, "login.gohtml", http.StatusOK, &LoginPageData{App: NewAppData(app), Error: err})
pw.writePage(w, req, "login.gohtml", http.StatusOK, &LoginPageData{App: NewAppData(app), CsrfField: csrfField, Error: err})
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<label for="password">Password</label>
<input type="password" id="password" name="password" placeholder="Password" />
</div>
{{ .CsrfField }}
<button type="submit" name="submit">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 8C16 12.4183 12.4183 16 8 16C3.58171 16 0 12.4183 0 8C0 3.58171 3.58171 0 8 0C12.4183 0 16 3.58171 16 8ZM7.07464 12.2359L13.0101 6.30045C13.2117 6.0989 13.2117 5.7721 13.0101 5.57055L12.2802 4.84064C12.0787 4.63906 11.7519 4.63906 11.5503 4.84064L6.70968 9.68123L4.44971 7.42126C4.24816 7.21971 3.92135 7.21971 3.71977 7.42126L2.98987 8.15116C2.78832 8.35271 2.78832 8.67952 2.98987 8.88106L6.34471 12.2359C6.54629 12.4375 6.87306 12.4375 7.07464 12.2359Z" fill="white"/>
Expand Down
5 changes: 5 additions & 0 deletions internal/pkg/service/appsproxy/proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2676,9 +2676,14 @@ func createDependencies(t *testing.T, sandboxesAPIURL string) (proxyDependencies
_, err := rand.Read(secret)
require.NoError(t, err)

csrfSecret := make([]byte, 32)
_, err = rand.Read(csrfSecret)
require.NoError(t, err)

cfg := config.New()
cfg.API.PublicURL, _ = url.Parse("https://hub.keboola.local")
cfg.CookieSecretSalt = string(secret)
cfg.CsrfTokenSalt = string(csrfSecret)
cfg.SandboxesAPI.URL = sandboxesAPIURL

return proxyDependencies.NewMockedServiceScope(t, cfg, dependencies.WithRealHTTPClient())
Expand Down
1 change: 1 addition & 0 deletions internal/pkg/service/appsproxy/proxy/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestAppProxyHandler(t *testing.T) {
cfg := config.New()
cfg.API.PublicURL, _ = url.Parse("https://hub.keboola.local")
cfg.SandboxesAPI.URL = appsAPI.URL
cfg.CsrfTokenSalt = "abc"

// Create dependencies
d, mocked := proxyDependencies.NewMockedServiceScope(t, cfg, dependencies.WithRealHTTPClient())
Expand Down
2 changes: 1 addition & 1 deletion provisioning/apps-proxy/dev/.air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ tmp_dir = "target/.watcher"

[build]
bin = "./target/apps-proxy/proxy"
args_bin = ["--sandboxes-api-url", "http://localhost:1080", "--sandboxes-api-token", "my-token", "--api-public-url", "http://localhost:8000", "--cookie-secret-salt", "cookie"]
args_bin = ["--sandboxes-api-url", "http://localhost:1080", "--sandboxes-api-token", "my-token", "--api-public-url", "http://localhost:8000", "--cookie-secret-salt", "cookie", "--csrf-token-salt", "bcc3add3bf72e628149fbfbc11932329de7f375db3d8503ef0e32b336adf46c4"]
cmd = "make build-apps-proxy"
delay = 2000
exclude_dir = []
Expand Down