improved oauth frontend configuration; better separation of concerns (#411)

This commit is contained in:
Michael Quigley 2023-10-18 11:47:26 -04:00
parent 1566efe637
commit 509bea7fc2
No known key found for this signature in database
GPG Key ID: 9B60314A9DD20A62
7 changed files with 50 additions and 69 deletions

View File

@ -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 # v0.4.9
FIX: Remove extraneous share token prepended to OAuth frontend redirect. FIX: Remove extraneous share token prepended to OAuth frontend redirect.

View File

@ -90,23 +90,25 @@ The public frontend configuration includes a new `oauth` section:
```yaml ```yaml
oauth: oauth:
redirect_host: oauth.zrok.io bind_address: 0.0.0.0:8181
redirect_port: 28080 redirect_url: https://oauth.zrok.io
redirect_http_only: false cookie_domain: zrok.io
hash_key: "<yourRandomHashKey>" hash_key: "the quick brown fox jumped over the lazy dog"
providers: providers:
- name: google - name: google
client_id: <client-id> client_id: "<client id from google>"
client_secret: <client-secret> client_secret: "<client secret from google>"
- name: github - name: github
client_id: <client-id> client_id: "<client id from github>"
client_secret: <client-secret> 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. `hash_key` is a unique string for your installation that is used to secure the authentication payloads for your public frontend.

View File

@ -2,15 +2,13 @@ package publicProxy
import ( import (
"context" "context"
"fmt"
"github.com/michaelquigley/cf" "github.com/michaelquigley/cf"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
zhttp "github.com/zitadel/oidc/v2/pkg/http" zhttp "github.com/zitadel/oidc/v2/pkg/http"
"strings"
) )
const V = 2 const V = 3
type Config struct { type Config struct {
V int V int
@ -21,11 +19,11 @@ type Config struct {
} }
type OauthConfig struct { type OauthConfig struct {
RedirectHost string BindAddress string
RedirectPort int RedirectUrl string
RedirectHttpOnly bool CookieDomain string
HashKey string `cf:"+secret"` HashKey string `cf:"+secret"`
Providers []*OauthProviderConfig Providers []*OauthProviderConfig
} }
func (oc *OauthConfig) GetProvider(name string) *OauthProviderConfig { func (oc *OauthConfig) GetProvider(name string) *OauthProviderConfig {
@ -71,6 +69,6 @@ func configureOauthHandlers(ctx context.Context, cfg *Config, tls bool) error {
if err := configureGithubOauth(cfg.Oauth, tls); err != nil { if err := configureGithubOauth(cfg.Oauth, tls); err != nil {
return err 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 return nil
} }

View File

@ -16,7 +16,6 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"time" "time"
) )
@ -32,12 +31,10 @@ func configureGithubOauth(cfg *OauthConfig, tls bool) error {
return nil return nil
} }
clientID := providerCfg.ClientId clientID := providerCfg.ClientId
callbackPath := "/github/oauth"
redirectUrl := fmt.Sprintf("%s://%s", scheme, cfg.RedirectHost)
rpConfig := &oauth2.Config{ rpConfig := &oauth2.Config{
ClientID: clientID, ClientID: clientID,
ClientSecret: providerCfg.ClientSecret, 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"}, Scopes: []string{"user:email"},
Endpoint: githubOAuth.Endpoint, Endpoint: githubOAuth.Endpoint,
} }
@ -52,15 +49,7 @@ func configureGithubOauth(cfg *OauthConfig, tls bool) error {
} }
key := hash.Sum(nil) key := hash.Sum(nil)
u, err := url.Parse(redirectUrl) cookieHandler := zhttp.NewCookieHandler(key, key, zhttp.WithUnsecure(), zhttp.WithDomain(cfg.CookieDomain))
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))
options := []rp.Option{ options := []rp.Option{
rp.WithCookieHandler(cookieHandler), rp.WithCookieHandler(cookieHandler),
@ -177,10 +166,10 @@ func configureGithubOauth(cfg *OauthConfig, tls bool) error {
authCheckInterval = i 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.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 return nil
} }

View File

@ -16,7 +16,6 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"time" "time"
) )
@ -33,12 +32,10 @@ func configureGoogleOauth(cfg *OauthConfig, tls bool) error {
} }
clientID := providerCfg.ClientId clientID := providerCfg.ClientId
callbackPath := "/google/oauth"
redirectUrl := fmt.Sprintf("%s://%s", scheme, cfg.RedirectHost)
rpConfig := &oauth2.Config{ rpConfig := &oauth2.Config{
ClientID: clientID, ClientID: clientID,
ClientSecret: providerCfg.ClientSecret, 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"}, Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"},
Endpoint: googleOauth.Endpoint, Endpoint: googleOauth.Endpoint,
} }
@ -53,15 +50,7 @@ func configureGoogleOauth(cfg *OauthConfig, tls bool) error {
} }
key := hash.Sum(nil) key := hash.Sum(nil)
u, err := url.Parse(redirectUrl) cookieHandler := zhttp.NewCookieHandler(key, key, zhttp.WithUnsecure(), zhttp.WithDomain(cfg.CookieDomain))
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))
options := []rp.Option{ options := []rp.Option{
rp.WithCookieHandler(cookieHandler), rp.WithCookieHandler(cookieHandler),
@ -157,10 +146,10 @@ func configureGoogleOauth(cfg *OauthConfig, tls bool) error {
authCheckInterval = i 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.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 return nil
} }

View File

@ -228,7 +228,7 @@ func authHandler(handler http.Handler, pcfg *Config, key []byte, ctx ziti.Contex
cookie, err := r.Cookie("zrok-access") cookie, err := r.Cookie("zrok-access")
if err != nil { if err != nil {
logrus.Errorf("unable to get 'zrok-access' cookie: %v", err) 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 return
} }
tkn, err := jwt.ParseWithClaims(cookie.Value, &ZrokClaims{}, func(t *jwt.Token) (interface{}, error) { tkn, err := jwt.ParseWithClaims(cookie.Value, &ZrokClaims{}, func(t *jwt.Token) (interface{}, error) {
@ -239,18 +239,18 @@ func authHandler(handler http.Handler, pcfg *Config, key []byte, ctx ziti.Contex
}) })
if err != nil { if err != nil {
logrus.Errorf("unable to parse jwt: %v", err) 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 return
} }
claims := tkn.Claims.(*ZrokClaims) claims := tkn.Claims.(*ZrokClaims)
if claims.Provider != provider { if claims.Provider != provider {
logrus.Error("provider mismatch; restarting auth flow") 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 return
} }
if claims.AuthorizationCheckInterval != authCheckInterval { if claims.AuthorizationCheckInterval != authCheckInterval {
logrus.Error("authorization check interval mismatch; restarting auth flow") 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 return
} }
if validDomains, found := oauthCfg.(map[string]interface{})["email_domains"]; found { if validDomains, found := oauthCfg.(map[string]interface{})["email_domains"]; found {
@ -347,12 +347,8 @@ func basicAuthRequired(w http.ResponseWriter, realm string) {
_, _ = w.Write([]byte("No Authorization\n")) _, _ = w.Write([]byte("No Authorization\n"))
} }
func oauthLoginRequired(w http.ResponseWriter, r *http.Request, shrToken string, pcfg *Config, provider, target string, authCheckInterval time.Duration) { func oauthLoginRequired(w http.ResponseWriter, r *http.Request, cfg *OauthConfig, provider, target string, authCheckInterval time.Duration) {
scheme := "https" http.Redirect(w, r, fmt.Sprintf("%s/%s/login?targethost=%s&checkInterval=%s", cfg.RedirectUrl, provider, url.QueryEscape(target), authCheckInterval.String()), http.StatusFound)
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 resolveService(hostMatch string, host string) string { func resolveService(hostMatch string, host string) string {

View File

@ -2,7 +2,7 @@
# configuration, the software will expect this field to be incremented. This protects you against invalid configuration # 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. # 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 # 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 # configured string. This will allow you to let a load balancer access the frontend by IP address for health check
@ -13,16 +13,19 @@ v: 2
# The OAuth configuration is used when enabling OAuth authentication with your public frontend. # The OAuth configuration is used when enabling OAuth authentication with your public frontend.
# #
#oauth: #oauth:
# # `redirect_host` and `redirect_port` should correspond with the DNS hostname and URL representing # # `bind_address` is the <address:port> of the interface where the OAuth frontend listener should
# # the OAuth frontend you'll use with your installation. # # bind
# # # #
# redirect_host: oauth.zrok.io # bind_address: 127.0.0.1:8181
# redirect_port: 28080
# #
# # `redirect_http_only` will generate an HTTP URI for your OAuth frontend, rather than HTTPS. This # # `redirect_url` is the <scheme://address[:port]> of the URL where OAuth requests should be directed.
# # should only be set to `true` in development environments.
# # # #
# 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 # # `hash_key` is a unique key for your installation that is used to secure authentication payloads
# # with OAuth providers. # # with OAuth providers.