Skip to content

Commit

Permalink
Support oc web login in proxy (#382)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeykazakov authored Jan 11, 2024
1 parent 74cff47 commit 879584a
Show file tree
Hide file tree
Showing 6 changed files with 709 additions and 504 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ go 1.20

require (
github.com/aws/aws-sdk-go v1.44.100
github.com/codeready-toolchain/api v0.0.0-20231129193441-f6c9b7feee01
github.com/codeready-toolchain/toolchain-common v0.0.0-20231206125932-41dd47e8aa56
github.com/codeready-toolchain/api v0.0.0-20240103194050-d5c7803671c1
github.com/codeready-toolchain/toolchain-common v0.0.0-20240103195541-637ca99d891b
github.com/go-logr/logr v1.2.3
github.com/gofrs/uuid v4.2.0+incompatible
github.com/pkg/errors v0.9.1
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,10 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/codeready-toolchain/api v0.0.0-20231129193441-f6c9b7feee01 h1:Pl8UIl/m2/n9C4pXzaR6A3vxRFX+4QRw90x8RDhCdXw=
github.com/codeready-toolchain/api v0.0.0-20231129193441-f6c9b7feee01/go.mod h1:FO7kgXH1x1LqkF327D5a36u0WIrwjVCbeijPkzgwaZc=
github.com/codeready-toolchain/toolchain-common v0.0.0-20231206125932-41dd47e8aa56 h1:W/VrNwL++P6TRw6BqhPfnzaq3YmytBIvestbLDvVv1E=
github.com/codeready-toolchain/toolchain-common v0.0.0-20231206125932-41dd47e8aa56/go.mod h1:cEkJH2jz88KIZt5W8wSnK3Gz6OfszzXv74OIndbTlRE=
github.com/codeready-toolchain/api v0.0.0-20240103194050-d5c7803671c1 h1:R+5BmQrz9hBfj6QFL+ojExD6CiZ5EuuVUbeb3pqxdds=
github.com/codeready-toolchain/api v0.0.0-20240103194050-d5c7803671c1/go.mod h1:FO7kgXH1x1LqkF327D5a36u0WIrwjVCbeijPkzgwaZc=
github.com/codeready-toolchain/toolchain-common v0.0.0-20240103195541-637ca99d891b h1:Is479bj8oC3/p/MDhtlBbadAh711YnKd6aBod9ef//k=
github.com/codeready-toolchain/toolchain-common v0.0.0-20240103195541-637ca99d891b/go.mod h1:/5KdPDrtN6ksuReL0/IuqV9kLsumkVbt/EjZDBmb+Ig=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down
8 changes: 8 additions & 0 deletions pkg/configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ func (r AuthConfig) AuthClientPublicKeysURL() string {
return commonconfig.GetString(r.c.AuthClientPublicKeysURL, "https://sso.devsandbox.dev/auth/realms/sandbox-dev/protocol/openid-connect/certs")
}

func (r AuthConfig) SSOBaseURL() string {
return commonconfig.GetString(r.c.SSOBaseURL, "https://sso.devsandbox.dev")
}

func (r AuthConfig) SSORealm() string {
return commonconfig.GetString(r.c.SSORealm, "sandbox-dev")
}

type VerificationConfig struct {
c toolchainv1alpha1.RegistrationServiceVerificationConfig
secrets map[string]map[string]string
Expand Down
6 changes: 6 additions & 0 deletions pkg/configuration/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ func TestRegistrationService(t *testing.T) {
assert.Equal(t, `{"realm": "sandbox-dev","auth-server-url": "https://sso.devsandbox.dev/auth","ssl-required": "none","resource": "sandbox-public","clientId": "sandbox-public","public-client": true, "confidential-port": 0}`,
regServiceCfg.Auth().AuthClientConfigRaw())
assert.Equal(t, "https://sso.devsandbox.dev/auth/realms/sandbox-dev/protocol/openid-connect/certs", regServiceCfg.Auth().AuthClientPublicKeysURL())
assert.Equal(t, "https://sso.devsandbox.dev", regServiceCfg.Auth().SSOBaseURL())
assert.Equal(t, "sandbox-dev", regServiceCfg.Auth().SSORealm())
assert.False(t, regServiceCfg.Verification().Enabled())
assert.Equal(t, 5, regServiceCfg.Verification().DailyLimit())
assert.Equal(t, 3, regServiceCfg.Verification().AttemptsAllowed())
Expand Down Expand Up @@ -78,6 +80,8 @@ func TestRegistrationService(t *testing.T) {
Auth().AuthClientConfigContentType("application/xml").
Auth().AuthClientConfigRaw(`{"realm": "toolchain-private"}`).
Auth().AuthClientPublicKeysURL("https://sso.openshift.com/certs").
Auth().SSOBaseURL("https://sso.test.org").
Auth().SSORealm("my-realm").
Verification().Enabled(true).
Verification().DailyLimit(15).
Verification().AttemptsAllowed(13).
Expand Down Expand Up @@ -123,6 +127,8 @@ func TestRegistrationService(t *testing.T) {
assert.Equal(t, "application/xml", regServiceCfg.Auth().AuthClientConfigContentType())
assert.Equal(t, `{"realm": "toolchain-private"}`, regServiceCfg.Auth().AuthClientConfigRaw())
assert.Equal(t, "https://sso.openshift.com/certs", regServiceCfg.Auth().AuthClientPublicKeysURL())
assert.Equal(t, "https://sso.test.org", regServiceCfg.Auth().SSOBaseURL())
assert.Equal(t, "my-realm", regServiceCfg.Auth().SSORealm())

assert.True(t, regServiceCfg.Verification().Enabled())
assert.Equal(t, 15, regServiceCfg.Verification().DailyLimit())
Expand Down
114 changes: 109 additions & 5 deletions pkg/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/http"
"net/http/httputil"
"net/textproto"
"net/url"
"strings"
"time"
"unicode/utf8"
Expand Down Expand Up @@ -42,10 +43,24 @@ const (
ProxyPort = "8081"
bearerProtocolPrefix = "base64url.bearer.authorization.k8s.io." //nolint:gosec

proxyHealthEndpoint = "/proxyhealth"
pluginsEndpoint = "/plugins/"
proxyHealthEndpoint = "/proxyhealth"
authEndpoint = "/auth/"
wellKnownOauthConfigEndpoint = "/.well-known/oauth-authorization-server"
pluginsEndpoint = "/plugins/"
)

func ssoWellKnownTarget() string {
return fmt.Sprintf("%s/auth/realms/%s/.well-known/openid-configuration", configuration.GetRegistrationServiceConfig().Auth().SSOBaseURL(), configuration.GetRegistrationServiceConfig().Auth().SSORealm())
}

func openidAuthEndpoint() string {
return fmt.Sprintf("/auth/realms/%s/protocol/openid-connect/auth", configuration.GetRegistrationServiceConfig().Auth().SSORealm())
}

func authorizationEndpointTarget() string {
return fmt.Sprintf("%s%s", configuration.GetRegistrationServiceConfig().Auth().SSOBaseURL(), openidAuthEndpoint())
}

type Proxy struct {
app application.Application
cl client.Client
Expand Down Expand Up @@ -106,13 +121,12 @@ func (p *Proxy) StartProxy() *http.Server {
router.Use(
middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
Skipper: func(ctx echo.Context) bool {
return ctx.Request().URL.RequestURI() == proxyHealthEndpoint // skip logging for health check so it doesn't pollute the logs
return ctx.Request().URL.RequestURI() == proxyHealthEndpoint // skip logging for health check, so it doesn't pollute the logs
},
LogMethod: true,
LogStatus: true,
LogURI: true,
LogValuesFunc: func(ctx echo.Context, v middleware.RequestLoggerValues) error {

log.InfoEchof(ctx, "request routed")
return nil
},
Expand All @@ -121,9 +135,25 @@ func (p *Proxy) StartProxy() *http.Server {

// routes
wg := router.Group("/apis/toolchain.dev.openshift.com/v1alpha1/workspaces")
// Space lister routes
wg.GET("/:workspace", handlers.HandleSpaceGetRequest(p.spaceLister))
wg.GET("", handlers.HandleSpaceListRequest(p.spaceLister))
router.GET(proxyHealthEndpoint, p.health)
// SSO routes. Used by web login (oc login -w).
// Here is the expected flow for the "oc login -w" command:
// 1. "oc login -w <proxy_url>"
// 2. oc calls <proxy_url>/.well-known/oauth-authorization-server (wellKnownOauthConfigEndpoint endpoint)
// 3. proxy forwards it to <sso_url>/auth/realms/<sso_realm>/.well-known/openid-configuration
// 4. oc starts an OAuth flow by opening a browser for <proxy_url>/auth/realms/<realm>/protocol/openid-connect/auth
// 5. proxy redirects (the request is not proxied but redirected via 403 See Others response!) the request
// to <sso_url>/auth/realms/<realm>/protocol/openid-connect/auth
// Note: oc uses this hardcoded public (no secret) oauth client name: "openshift-cli-client" which has to exist in SSO to make this flow work.
// 6. user provides the login credentials in the sso login page
// 7. all following oc requests (<proxy_url>/auth/*) go to the proxy and forwarded to SSO as is. This is used to obtain the generated token by oc.
router.Any(wellKnownOauthConfigEndpoint, p.oauthConfiguration) // <- this is the step 2 in the flow above
router.Any(fmt.Sprintf("%s*", openidAuthEndpoint()), p.openidAuth) // <- this is the step 5 in the flow above
router.Any(fmt.Sprintf("%s*", authEndpoint), p.auth) // <- this is the step 7.
// The main proxy route
router.Any("/*", p.handleRequestAndRedirect)

// Insert the CORS preflight middleware
Expand All @@ -149,6 +179,80 @@ func (p *Proxy) StartProxy() *http.Server {
return srv
}

// unsecured returns true if the request does not require authentication
func unsecured(ctx echo.Context) bool {
uri := ctx.Request().URL.RequestURI()
return uri == proxyHealthEndpoint || uri == wellKnownOauthConfigEndpoint || strings.HasPrefix(uri, authEndpoint)
}

// auth handles requests to SSO. Used by web login.
func (p *Proxy) auth(ctx echo.Context) error {
req := ctx.Request()
targetURL, err := url.Parse(configuration.GetRegistrationServiceConfig().Auth().SSOBaseURL())
if err != nil {
return err
}
targetURL.Path = req.URL.Path
targetURL.RawQuery = req.URL.RawQuery

return p.handleSSORequest(targetURL)(ctx)
}

// oauthConfiguration handles requests to oauth configuration and proxies them to the corresponding SSO endpoint. Used by web login.
func (p *Proxy) oauthConfiguration(ctx echo.Context) error {
targetURL, err := url.Parse(ssoWellKnownTarget())
if err != nil {
return err
}
return p.handleSSORequest(targetURL)(ctx)
}

// openidAuth handles requests to the openID Connect authentication endpoint. Used by web login.
func (p *Proxy) openidAuth(ctx echo.Context) error {
targetURL, err := url.Parse(authorizationEndpointTarget())
if err != nil {
return err
}
targetURL.Path = ctx.Request().URL.Path
targetURL.RawQuery = ctx.Request().URL.RawQuery

// Let's redirect the browser's request to the SSO authentication page instead of proxying it
// in order to avoid passing the user's login credentials through our proxy.
return p.redirectTo(ctx, targetURL.String())
}

func (p *Proxy) redirectTo(ctx echo.Context, to string) error {
log.InfoEchof(ctx, "redirecting %s to %s", ctx.Request().URL.String(), to)
http.Redirect(ctx.Response().Writer, ctx.Request(), to, http.StatusSeeOther)
return nil
}

// handleSSORequest handles requests to the cluster authentication server and proxy them to SSO instead. Used by web login.
func (p *Proxy) handleSSORequest(targetURL *url.URL) echo.HandlerFunc {
return func(ctx echo.Context) error {
req := ctx.Request()
director := func(req *http.Request) {
origin := req.URL.String()
req.URL.Scheme = targetURL.Scheme
req.URL.Host = targetURL.Host
req.URL.Path = targetURL.Path
req.URL.RawQuery = targetURL.RawQuery
req.Host = targetURL.Host
log.InfoEchof(ctx, "forwarding %s to %s", origin, req.URL.String())
}
transport := getTransport(req.Header)
reverseProxy := &httputil.ReverseProxy{
Director: director,
Transport: transport,
FlushInterval: -1,
}

// Note that ServeHttp is non-blocking and uses a go routine under the hood
reverseProxy.ServeHTTP(ctx.Response().Writer, ctx.Request())
return nil
}
}

func (p *Proxy) health(ctx echo.Context) error {
ctx.Response().Writer.Header().Set("Content-Type", "application/json")
ctx.Response().Writer.WriteHeader(http.StatusOK)
Expand Down Expand Up @@ -280,7 +384,7 @@ func customHTTPErrorHandler(cause error, ctx echo.Context) {
func (p *Proxy) addUserContext() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
if ctx.Request().URL.Path == proxyHealthEndpoint { // skip only for health endpoint
if unsecured(ctx) { // skip only for unsecured endpoints
return next(ctx)
}

Expand Down
Loading

0 comments on commit 879584a

Please sign in to comment.