Skip to content

Commit

Permalink
improved oauth frontend configuration; better separation of concerns (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelquigley committed Oct 18, 2023
1 parent 1566efe commit 509bea7
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 69 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# v0.4.10

CHANGE: The public frontend configuration has been bumped from `v: 2` to `v: 3`. The `redirect_host`, `redirect_port` and `redirect_http_only` parameters have been removed. These three configuration options have been replaced with `bind_address`, `redirect_url` and `cookie_domain`. See the OAuth configuration guide at `docs/guides/self-hosting/oauth/configuring-oauth.md` for more details (https://github.com/openziti/zrok/issues/411)

# v0.4.9

FIX: Remove extraneous share token prepended to OAuth frontend redirect.
Expand Down
26 changes: 14 additions & 12 deletions docs/guides/self-hosting/oauth/configuring-oauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,25 @@ The public frontend configuration includes a new `oauth` section:

```yaml
oauth:
redirect_host: oauth.zrok.io
redirect_port: 28080
redirect_http_only: false
hash_key: "<yourRandomHashKey>"
bind_address: 0.0.0.0:8181
redirect_url: https://oauth.zrok.io
cookie_domain: zrok.io
hash_key: "the quick brown fox jumped over the lazy dog"
providers:
- name: google
client_id: <client-id>
client_secret: <client-secret>
- name: github
client_id: <client-id>
client_secret: <client-secret>
- name: google
client_id: "<client id from google>"
client_secret: "<client secret from google>"
- name: github
client_id: "<client id from github>"
client_secret: "<client secret from github>"

```
The `redirect_host` and `redirect_port` value should correspond with the DNS hostname and port configured as your OAuth frontend.
The `bind_address` parameter determines where the OAuth frontend will bind. Should be in `ip:port` format.

The `redirect_http_only` is useful in development environments where your OAuth frontend is not running behind an HTTPS reverse proxy. Should not be enabled in production environments!
The `redirect_url` parameter determines the base URL where OAuth frontend requests will be redirected.

`cookie_domain` is the domain where authentication cookies should be stored.

`hash_key` is a unique string for your installation that is used to secure the authentication payloads for your public frontend.

Expand Down
16 changes: 7 additions & 9 deletions endpoints/publicProxy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ package publicProxy

import (
"context"
"fmt"
"github.com/michaelquigley/cf"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
zhttp "github.com/zitadel/oidc/v2/pkg/http"
"strings"
)

const V = 2
const V = 3

type Config struct {
V int
Expand All @@ -21,11 +19,11 @@ type Config struct {
}

type OauthConfig struct {
RedirectHost string
RedirectPort int
RedirectHttpOnly bool
HashKey string `cf:"+secret"`
Providers []*OauthProviderConfig
BindAddress string
RedirectUrl string
CookieDomain string
HashKey string `cf:"+secret"`
Providers []*OauthProviderConfig
}

func (oc *OauthConfig) GetProvider(name string) *OauthProviderConfig {
Expand Down Expand Up @@ -71,6 +69,6 @@ func configureOauthHandlers(ctx context.Context, cfg *Config, tls bool) error {
if err := configureGithubOauth(cfg.Oauth, tls); err != nil {
return err
}
zhttp.StartServer(ctx, fmt.Sprintf("%s:%d", strings.Split(cfg.Address, ":")[0], cfg.Oauth.RedirectPort))
zhttp.StartServer(ctx, cfg.Oauth.BindAddress)
return nil
}
19 changes: 4 additions & 15 deletions endpoints/publicProxy/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"io"
"net/http"
"net/url"
"strings"
"time"
)

Expand All @@ -32,12 +31,10 @@ func configureGithubOauth(cfg *OauthConfig, tls bool) error {
return nil
}
clientID := providerCfg.ClientId
callbackPath := "/github/oauth"
redirectUrl := fmt.Sprintf("%s://%s", scheme, cfg.RedirectHost)
rpConfig := &oauth2.Config{
ClientID: clientID,
ClientSecret: providerCfg.ClientSecret,
RedirectURL: fmt.Sprintf("%v:%v%v", redirectUrl, cfg.RedirectPort, callbackPath),
RedirectURL: fmt.Sprintf("%v/github/oauth", cfg.RedirectUrl),
Scopes: []string{"user:email"},
Endpoint: githubOAuth.Endpoint,
}
Expand All @@ -52,15 +49,7 @@ func configureGithubOauth(cfg *OauthConfig, tls bool) error {
}
key := hash.Sum(nil)

u, err := url.Parse(redirectUrl)
if err != nil {
logrus.Errorf("unable to parse redirect url: %v", err)
return err
}
parts := strings.Split(u.Hostname(), ".")
domain := parts[len(parts)-2] + "." + parts[len(parts)-1]

cookieHandler := zhttp.NewCookieHandler(key, key, zhttp.WithUnsecure(), zhttp.WithDomain(domain))
cookieHandler := zhttp.NewCookieHandler(key, key, zhttp.WithUnsecure(), zhttp.WithDomain(cfg.CookieDomain))

options := []rp.Option{
rp.WithCookieHandler(cookieHandler),
Expand Down Expand Up @@ -177,10 +166,10 @@ func configureGithubOauth(cfg *OauthConfig, tls bool) error {
authCheckInterval = i
}

SetZrokCookie(w, domain, primaryEmail, tokens.AccessToken, "github", authCheckInterval, key)
SetZrokCookie(w, cfg.CookieDomain, primaryEmail, tokens.AccessToken, "github", authCheckInterval, key)
http.Redirect(w, r, fmt.Sprintf("%s://%s", scheme, token.Claims.(*IntermediateJWT).Host), http.StatusFound)
}

http.Handle(callbackPath, rp.CodeExchangeHandler(getEmail, relyingParty))
http.Handle("/github/oauth", rp.CodeExchangeHandler(getEmail, relyingParty))
return nil
}
19 changes: 4 additions & 15 deletions endpoints/publicProxy/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"io"
"net/http"
"net/url"
"strings"
"time"
)

Expand All @@ -33,12 +32,10 @@ func configureGoogleOauth(cfg *OauthConfig, tls bool) error {
}

clientID := providerCfg.ClientId
callbackPath := "/google/oauth"
redirectUrl := fmt.Sprintf("%s://%s", scheme, cfg.RedirectHost)
rpConfig := &oauth2.Config{
ClientID: clientID,
ClientSecret: providerCfg.ClientSecret,
RedirectURL: fmt.Sprintf("%v:%v%v", redirectUrl, cfg.RedirectPort, callbackPath),
RedirectURL: fmt.Sprintf("%v/google/oauth", cfg.RedirectUrl),
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"},
Endpoint: googleOauth.Endpoint,
}
Expand All @@ -53,15 +50,7 @@ func configureGoogleOauth(cfg *OauthConfig, tls bool) error {
}
key := hash.Sum(nil)

u, err := url.Parse(redirectUrl)
if err != nil {
logrus.Errorf("unable to parse redirect url: %v", err)
return err
}
parts := strings.Split(u.Hostname(), ".")
domain := parts[len(parts)-2] + "." + parts[len(parts)-1]

cookieHandler := zhttp.NewCookieHandler(key, key, zhttp.WithUnsecure(), zhttp.WithDomain(domain))
cookieHandler := zhttp.NewCookieHandler(key, key, zhttp.WithUnsecure(), zhttp.WithDomain(cfg.CookieDomain))

options := []rp.Option{
rp.WithCookieHandler(cookieHandler),
Expand Down Expand Up @@ -157,10 +146,10 @@ func configureGoogleOauth(cfg *OauthConfig, tls bool) error {
authCheckInterval = i
}

SetZrokCookie(w, domain, rDat.Email, tokens.AccessToken, "google", authCheckInterval, key)
SetZrokCookie(w, cfg.CookieDomain, rDat.Email, tokens.AccessToken, "google", authCheckInterval, key)
http.Redirect(w, r, fmt.Sprintf("%s://%s", scheme, token.Claims.(*IntermediateJWT).Host), http.StatusFound)
}

http.Handle(callbackPath, rp.CodeExchangeHandler(getEmail, relyingParty))
http.Handle("/google/oauth", rp.CodeExchangeHandler(getEmail, relyingParty))
return nil
}
16 changes: 6 additions & 10 deletions endpoints/publicProxy/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func authHandler(handler http.Handler, pcfg *Config, key []byte, ctx ziti.Contex
cookie, err := r.Cookie("zrok-access")
if err != nil {
logrus.Errorf("unable to get 'zrok-access' cookie: %v", err)
oauthLoginRequired(w, r, shrToken, pcfg, provider.(string), target, authCheckInterval)
oauthLoginRequired(w, r, pcfg.Oauth, provider.(string), target, authCheckInterval)
return
}
tkn, err := jwt.ParseWithClaims(cookie.Value, &ZrokClaims{}, func(t *jwt.Token) (interface{}, error) {
Expand All @@ -239,18 +239,18 @@ func authHandler(handler http.Handler, pcfg *Config, key []byte, ctx ziti.Contex
})
if err != nil {
logrus.Errorf("unable to parse jwt: %v", err)
oauthLoginRequired(w, r, shrToken, pcfg, provider.(string), target, authCheckInterval)
oauthLoginRequired(w, r, pcfg.Oauth, provider.(string), target, authCheckInterval)
return
}
claims := tkn.Claims.(*ZrokClaims)
if claims.Provider != provider {
logrus.Error("provider mismatch; restarting auth flow")
oauthLoginRequired(w, r, shrToken, pcfg, provider.(string), target, authCheckInterval)
oauthLoginRequired(w, r, pcfg.Oauth, provider.(string), target, authCheckInterval)
return
}
if claims.AuthorizationCheckInterval != authCheckInterval {
logrus.Error("authorization check interval mismatch; restarting auth flow")
oauthLoginRequired(w, r, shrToken, pcfg, provider.(string), target, authCheckInterval)
oauthLoginRequired(w, r, pcfg.Oauth, provider.(string), target, authCheckInterval)
return
}
if validDomains, found := oauthCfg.(map[string]interface{})["email_domains"]; found {
Expand Down Expand Up @@ -347,12 +347,8 @@ func basicAuthRequired(w http.ResponseWriter, realm string) {
_, _ = w.Write([]byte("No Authorization\n"))
}

func oauthLoginRequired(w http.ResponseWriter, r *http.Request, shrToken string, pcfg *Config, provider, target string, authCheckInterval time.Duration) {
scheme := "https"
if pcfg.Oauth != nil && pcfg.Oauth.RedirectHttpOnly {
scheme = "http"
}
http.Redirect(w, r, fmt.Sprintf("%s://%s:%d/%s/login?targethost=%s&checkInterval=%s", scheme, pcfg.Oauth.RedirectHost, pcfg.Oauth.RedirectPort, provider, url.QueryEscape(target), authCheckInterval.String()), http.StatusFound)
func oauthLoginRequired(w http.ResponseWriter, r *http.Request, cfg *OauthConfig, provider, target string, authCheckInterval time.Duration) {
http.Redirect(w, r, fmt.Sprintf("%s/%s/login?targethost=%s&checkInterval=%s", cfg.RedirectUrl, provider, url.QueryEscape(target), authCheckInterval.String()), http.StatusFound)
}

func resolveService(hostMatch string, host string) string {
Expand Down
19 changes: 11 additions & 8 deletions etc/frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# configuration, the software will expect this field to be incremented. This protects you against invalid configuration
# versions and will refer to you to the documentation when the configuration structure changes.
#
v: 2
v: 3

# Setting the `host_match` setting will cause a `zrok access public` to ignore `Host` headers that do not contain the
# configured string. This will allow you to let a load balancer access the frontend by IP address for health check
Expand All @@ -13,16 +13,19 @@ v: 2
# The OAuth configuration is used when enabling OAuth authentication with your public frontend.
#
#oauth:
# # `redirect_host` and `redirect_port` should correspond with the DNS hostname and URL representing
# # the OAuth frontend you'll use with your installation.
# # `bind_address` is the <address:port> of the interface where the OAuth frontend listener should
# # bind
# #
# redirect_host: oauth.zrok.io
# redirect_port: 28080
# bind_address: 127.0.0.1:8181
#
# # `redirect_http_only` will generate an HTTP URI for your OAuth frontend, rather than HTTPS. This
# # should only be set to `true` in development environments.
# # `redirect_url` is the <scheme://address[:port]> of the URL where OAuth requests should be directed.
# #
# redirect_http_only: false
# redirect_url: https://oauth.zrok.io
#
# # `cookie_domain` is the domain where the authentication cookies should be applied. Should likely match
# # the `host_match` specified above.
# #
# cookie_domain: zrok.io
#
# # `hash_key` is a unique key for your installation that is used to secure authentication payloads
# # with OAuth providers.
Expand Down

0 comments on commit 509bea7

Please sign in to comment.